diff --git "a/ml_complete-all-topics/app.js" "b/ml_complete-all-topics/app.js" --- "a/ml_complete-all-topics/app.js" +++ "b/ml_complete-all-topics/app.js" @@ -1,1941 +1,4885 @@ -// ===== CONFIGURATION & CONSTANTS ===== -const COLORS = { - primary: '#4a90e2', - cyan: '#64ffda', - orange: '#ff6b6b', - green: '#51cf66', - background: '#0f3460', - text: '#e1e1e1', - textSecondary: '#a0a0a0' +// Data +const data = { + linearRegression: [ + { experience: 1, salary: 39.764 }, + { experience: 2, salary: 48.900 }, + { experience: 3, salary: 56.978 }, + { experience: 4, salary: 68.290 }, + { experience: 5, salary: 77.867 }, + { experience: 6, salary: 85.022 } + ], + logistic: [ + { height: 150, label: 0, prob: 0.2 }, + { height: 160, label: 0, prob: 0.35 }, + { height: 170, label: 0, prob: 0.5 }, + { height: 180, label: 1, prob: 0.65 }, + { height: 190, label: 1, prob: 0.8 }, + { height: 200, label: 1, prob: 0.9 } + ], + svm: [ + { label: 'A', x1: 2, x2: 7, class: 1 }, + { label: 'B', x1: 3, x2: 8, class: 1 }, + { label: 'C', x1: 4, x2: 7, class: 1 }, + { label: 'D', x1: 6, x2: 2, class: -1 }, + { label: 'E', x1: 7, x2: 3, class: -1 }, + { label: 'F', x1: 8, x2: 2, class: -1 } + ], + knn: [ + { x: 1, y: 2, class: 'orange' }, + { x: 0.9, y: 1.7, class: 'orange' }, + { x: 1.5, y: 2.5, class: 'orange' }, + { x: 4, y: 5, class: 'yellow' }, + { x: 4.2, y: 4.8, class: 'yellow' }, + { x: 3.8, y: 5.2, class: 'yellow' } + ], + roc: [ + { id: 'A', true_label: 1, score: 0.95 }, + { id: 'B', true_label: 0, score: 0.70 }, + { id: 'C', true_label: 1, score: 0.60 }, + { id: 'D', true_label: 0, score: 0.40 }, + { id: 'E', true_label: 1, score: 0.20 } + ] }; -const chartColors = ['#1FB8CD', '#FFC185', '#B4413C', '#ECEBD5', '#5D878F', '#DB4545', '#D2BA4C', '#964325', '#944454', '#13343B']; +// State +let state = { + slope: 7.5, + intercept: 32, + learningRate: 0.1, + gdIterations: [], + testPoint: { x: 2, y: 1 }, + svm: { + w1: 1, + w2: 1, + b: -10, + C: 1, + kernel: 'linear', + kernelParam: 1, + training: { + w: [0, 0], + b: 0, + step: 0, + learningRate: 0.01, + isTraining: false + } + } +}; -// ===== STATE MANAGEMENT ===== -let currentTopic = 1; -let currentSubject = 'statistics'; -let animationFrames = {}; +// Initialize category navigation +function initCategories() { + const categoryHeaders = document.querySelectorAll('.toc-category-header'); + + categoryHeaders.forEach(header => { + header.addEventListener('click', () => { + const category = header.getAttribute('data-category'); + const content = document.getElementById(`${category}-content`); + const toggle = header.querySelector('.category-toggle'); + + if (content.classList.contains('collapsed')) { + content.classList.remove('collapsed'); + toggle.classList.remove('collapsed'); + } else { + content.classList.add('collapsed'); + toggle.classList.add('collapsed'); + } + }); + }); + + // Start with all categories expanded + document.querySelectorAll('.toc-category-content').forEach(content => { + content.classList.remove('collapsed'); + }); +} -// ===== INITIALIZATION ===== -document.addEventListener('DOMContentLoaded', () => { - initNavigation(); - initSubjectTabs(); - initInteractiveElements(); - setupScrollObserver(); - initializeAllVisualizations(); +// Initialize collapsible sections +function initSections() { + const sections = document.querySelectorAll('.section'); + + sections.forEach(section => { + const header = section.querySelector('.section-header'); + const toggle = section.querySelector('.section-toggle'); + const body = section.querySelector('.section-body'); + + // Start with first section expanded + if (section.id === 'intro') { + body.classList.add('expanded'); + toggle.classList.remove('collapsed'); + } else { + toggle.classList.add('collapsed'); + } - // Show initial subject - switchSubject('statistics'); -}); + header.addEventListener('click', () => { + const isExpanded = body.classList.contains('expanded'); + + if (isExpanded) { + body.classList.remove('expanded'); + toggle.classList.add('collapsed'); + } else { + body.classList.add('expanded'); + toggle.classList.remove('collapsed'); + + // Initialize visualizations when section opens + if (section.id === 'linear-regression') initLinearRegression(); + if (section.id === 'gradient-descent') initGradientDescent(); + if (section.id === 'logistic-regression') initLogistic(); + if (section.id === 'svm') initSVM(); + if (section.id === 'knn') initKNN(); + if (section.id === 'model-evaluation') initModelEvaluation(); + if (section.id === 'regularization') initRegularization(); + if (section.id === 'bias-variance') initBiasVariance(); + if (section.id === 'cross-validation') initCrossValidation(); + if (section.id === 'preprocessing') initPreprocessing(); + if (section.id === 'loss-functions') initLossFunctions(); + if (section.id === 'optimal-k') initOptimalK(); + if (section.id === 'hyperparameter-tuning') initHyperparameterTuning(); + if (section.id === 'naive-bayes') initNaiveBayes(); + if (section.id === 'kmeans') initKMeans(); + if (section.id === 'decision-trees') initDecisionTrees(); + if (section.id === 'ensemble-methods') initEnsembleMethods(); + if (section.id === 'rl-intro') { /* No viz for intro */ } + if (section.id === 'q-learning') { /* Add Q-learning viz if needed */ } + if (section.id === 'policy-gradient') { /* Add policy gradient viz if needed */ } + if (section.id === 'algorithm-comparison') initAlgorithmComparison(); + } + }); + }); +} -// ===== SUBJECT SWITCHING ===== -function initSubjectTabs() { - const tabs = document.querySelectorAll('.subject-tab'); - tabs.forEach(tab => { - tab.addEventListener('click', () => { - const subject = tab.dataset.subject; - switchSubject(subject); - }); +// Smooth scroll for TOC links +function initTOCLinks() { + const links = document.querySelectorAll('.toc-link'); + + links.forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const targetId = link.getAttribute('href').substring(1); + const target = document.getElementById(targetId); + + if (target) { + // Remove active from all links + links.forEach(l => l.classList.remove('active')); + link.classList.add('active'); + + // Scroll to target + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + + // Expand the section + const toggle = target.querySelector('.section-toggle'); + const body = target.querySelector('.section-body'); + body.classList.add('expanded'); + toggle.classList.remove('collapsed'); + } }); + }); + + // Update active link on scroll + let ticking = false; + window.addEventListener('scroll', () => { + if (!ticking) { + window.requestAnimationFrame(() => { + updateActiveLink(); + ticking = false; + }); + ticking = true; + } + }); } -function switchSubject(subject) { - currentSubject = subject; - - // Update active tab - document.querySelectorAll('.subject-tab').forEach(tab => { - tab.classList.remove('active'); - if (tab.dataset.subject === subject) { - tab.classList.add('active'); +function updateActiveLink() { + const sections = document.querySelectorAll('.section'); + const scrollPos = window.scrollY + 100; + + sections.forEach(section => { + const top = section.offsetTop; + const height = section.offsetHeight; + const id = section.getAttribute('id'); + + if (scrollPos >= top && scrollPos < top + height) { + document.querySelectorAll('.toc-link').forEach(link => { + link.classList.remove('active'); + if (link.getAttribute('href') === '#' + id) { + link.classList.add('active'); } - }); - - // Update sidebar title - const titles = { - 'statistics': 'Statistics Content', - 'linear-algebra': 'Linear Algebra Content', - 'calculus': 'Calculus Content', - 'data-science': 'Data Science Content', - 'machine-learning': 'Machine Learning Algorithms' - }; - const sidebarTitle = document.getElementById('sidebarTitle'); - if (sidebarTitle) { - sidebarTitle.textContent = titles[subject]; + }); } - - // Show/hide sidebar modules - document.querySelectorAll('.module').forEach(module => { - const moduleSubject = module.dataset.subject; - if (moduleSubject) { - module.style.display = moduleSubject === subject ? 'block' : 'none'; - } else { - // Statistics modules (no data-subject attribute) - module.style.display = subject === 'statistics' ? 'block' : 'none'; + }); +} + +// Linear Regression Visualization +function initLinearRegression() { + const canvas = document.getElementById('lr-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + + const slopeSlider = document.getElementById('slope-slider'); + const interceptSlider = document.getElementById('intercept-slider'); + const slopeVal = document.getElementById('slope-val'); + const interceptVal = document.getElementById('intercept-val'); + + if (slopeSlider) { + slopeSlider.addEventListener('input', (e) => { + state.slope = parseFloat(e.target.value); + slopeVal.textContent = state.slope.toFixed(1); + drawLinearRegression(); + }); + } + + if (interceptSlider) { + interceptSlider.addEventListener('input', (e) => { + state.intercept = parseFloat(e.target.value); + interceptVal.textContent = state.intercept.toFixed(1); + drawLinearRegression(); + }); + } + + drawLinearRegression(); +} + +let lrChart = null; + +function drawLinearRegression() { + const canvas = document.getElementById('lr-canvas'); + if (!canvas) return; + + // Destroy existing chart + if (lrChart) { + lrChart.destroy(); + } + + const ctx = canvas.getContext('2d'); + + // Calculate fitted line points + const fittedLine = []; + for (let x = 0; x <= 7; x += 0.1) { + fittedLine.push({ x: x, y: state.slope * x + state.intercept }); + } + + // Calculate MSE + let mse = 0; + data.linearRegression.forEach(point => { + const predicted = state.slope * point.experience + state.intercept; + const error = point.salary - predicted; + mse += error * error; + }); + mse /= data.linearRegression.length; + + lrChart = safeCreateChart(ctx, { + type: 'scatter', + data: { + datasets: [ + { + label: 'Data Points', + data: data.linearRegression.map(p => ({ x: p.experience, y: p.salary })), + backgroundColor: '#6aa9ff', + pointRadius: 8, + pointHoverRadius: 10 + }, + { + label: 'Fitted Line', + data: fittedLine, + type: 'line', + borderColor: '#ff8c6a', + borderWidth: 3, + fill: false, + pointRadius: 0, + tension: 0 + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { + display: true, + text: `Experience vs Salary (MSE: ${mse.toFixed(2)})`, + color: '#e8eef6', + font: { size: 16 } + }, + legend: { + labels: { color: '#a9b4c2' } + } + }, + scales: { + x: { + title: { display: true, text: 'Years of Experience', color: '#a9b4c2' }, + grid: { color: '#2a3544' }, + ticks: { color: '#a9b4c2' } + }, + y: { + title: { display: true, text: 'Salary ($k)', color: '#a9b4c2' }, + grid: { color: '#2a3544' }, + ticks: { color: '#a9b4c2' } } + } + } + }, 'Linear Regression Chart'); +} + +// Gradient Descent Visualization +function initGradientDescent() { + const canvas = document.getElementById('gd-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + + const runBtn = document.getElementById('run-gd'); + const resetBtn = document.getElementById('reset-gd'); + const lrSlider = document.getElementById('lr-slider'); + const lrVal = document.getElementById('lr-val'); + + if (lrSlider) { + lrSlider.addEventListener('input', (e) => { + state.learningRate = parseFloat(e.target.value); + lrVal.textContent = state.learningRate.toFixed(2); + }); + } + + if (runBtn) { + runBtn.addEventListener('click', runGradientDescent); + } + + if (resetBtn) { + resetBtn.addEventListener('click', () => { + state.gdIterations = []; + drawGradientDescent(); }); + } + + drawGradientDescent(); +} + +function runGradientDescent() { + state.gdIterations = []; + let m = 0, c = 20; // Start with poor values + const alpha = state.learningRate; + const iterations = 50; + + for (let i = 0; i < iterations; i++) { + let dm = 0, dc = 0; + const n = data.linearRegression.length; + + // Calculate gradients + data.linearRegression.forEach(point => { + const predicted = m * point.experience + c; + const error = predicted - point.salary; + dm += (2 / n) * error * point.experience; + dc += (2 / n) * error; + }); + + // Update parameters + m -= alpha * dm; + c -= alpha * dc; - // Show/hide topic sections - document.querySelectorAll('.topic-section').forEach(section => { - const sectionSubject = section.dataset.subject || 'statistics'; - section.style.display = sectionSubject === subject ? 'block' : 'none'; + // Calculate loss + let loss = 0; + data.linearRegression.forEach(point => { + const predicted = m * point.experience + c; + const error = point.salary - predicted; + loss += error * error; }); + loss /= n; + + state.gdIterations.push({ m, c, loss }); + } + + animateGradientDescent(); +} + +function animateGradientDescent() { + let step = 0; + const interval = setInterval(() => { + if (step >= state.gdIterations.length) { + clearInterval(interval); + return; + } - // Scroll to first topic of subject - const firstTopic = document.querySelector(`.topic-section[data-subject="${subject}"], .topic-section:not([data-subject])`); - if (firstTopic && subject !== 'statistics') { - setTimeout(() => { - firstTopic.scrollIntoView({ behavior: 'smooth', block: 'start' }); - }, 100); - } else if (subject === 'statistics') { - const statsFirst = document.getElementById('topic-1'); - if (statsFirst) { - setTimeout(() => { - statsFirst.scrollIntoView({ behavior: 'smooth', block: 'start' }); - }, 100); + const iteration = state.gdIterations[step]; + state.slope = iteration.m; + state.intercept = iteration.c; + + // Update linear regression chart + drawLinearRegression(); + drawGradientDescent(step); + + step++; + }, 50); +} + +let gdChart = null; + +function drawGradientDescent(currentStep = -1) { + const canvas = document.getElementById('gd-canvas'); + if (!canvas) return; + + if (state.gdIterations.length === 0) { + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = '#a9b4c2'; + ctx.font = '16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Click "Run Gradient Descent" to see the algorithm in action', canvas.width / 2, canvas.height / 2); + return; + } + + // Destroy existing chart + if (gdChart) { + gdChart.destroy(); + } + + const ctx = canvas.getContext('2d'); + const lossData = state.gdIterations.map((iter, i) => ({ x: i + 1, y: iter.loss })); + + gdChart = safeCreateChart(ctx, { + type: 'line', + data: { + datasets: [{ + label: 'Training Loss', + data: lossData, + borderColor: '#7ef0d4', + backgroundColor: 'rgba(126, 240, 212, 0.1)', + borderWidth: 3, + fill: true, + tension: 0.4, + pointRadius: currentStep >= 0 ? lossData.map((_, i) => i === currentStep ? 8 : 2) : 4, + pointBackgroundColor: currentStep >= 0 ? lossData.map((_, i) => i === currentStep ? '#ff8c6a' : '#7ef0d4') : '#7ef0d4' + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { + display: true, + text: currentStep >= 0 ? `Gradient Descent Progress (Step ${currentStep + 1}/${state.gdIterations.length})` : 'Gradient Descent Progress', + color: '#e8eef6', + font: { size: 16 } + }, + legend: { + labels: { color: '#a9b4c2' } } + }, + scales: { + x: { + title: { display: true, text: 'Iterations', color: '#a9b4c2' }, + grid: { color: '#2a3544' }, + ticks: { color: '#a9b4c2' } + }, + y: { + title: { display: true, text: 'Loss (MSE)', color: '#a9b4c2' }, + grid: { color: '#2a3544' }, + ticks: { color: '#a9b4c2' } + } + } } + }); } -// ===== NAVIGATION ===== -function initNavigation() { - // Mobile menu toggle - const mobileMenuBtn = document.getElementById('mobileMenuBtn'); - const sidebar = document.getElementById('sidebar'); - - if (mobileMenuBtn) { - mobileMenuBtn.addEventListener('click', () => { - sidebar.classList.toggle('active'); - }); +// Safe chart creation with error handling +function safeCreateChart(ctx, config, chartName) { + try { + if (!ctx) { + console.warn(`Canvas context not found for ${chartName}`); + return null; } + return new Chart(ctx, config); + } catch (error) { + console.error(`Chart creation failed for ${chartName}:`, error); + // Show fallback message + if (ctx && ctx.canvas && ctx.canvas.parentElement) { + ctx.canvas.parentElement.innerHTML = `

Visualization temporarily unavailable. Please refresh the page.

`; + } + return null; + } +} - // Topic link navigation - const topicLinks = document.querySelectorAll('.topic-link'); - topicLinks.forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - const topicId = link.getAttribute('data-topic'); - const target = document.getElementById(`topic-${topicId}`); - - if (target) { - target.scrollIntoView({ behavior: 'smooth', block: 'start' }); - updateActiveLink(topicId); - - // Close mobile menu if open - if (window.innerWidth <= 1024) { - sidebar.classList.remove('active'); - } - } - }); - }); +// Initialize everything when DOM is ready +function init() { + initCategories(); + initSections(); + initTOCLinks(); + + // Initialize first section visualizations + setTimeout(() => { + initLinearRegression(); + }, 100); } -function updateActiveLink(topicId) { - document.querySelectorAll('.topic-link').forEach(link => { - link.classList.remove('active'); +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} + +// SVM Visualizations +function initSVM() { + initSVMBasic(); + initSVMMargin(); + initSVMCParameter(); + initSVMTraining(); + initSVMKernel(); +} + +function initSVMBasic() { + const canvas = document.getElementById('svm-basic-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + + const w1Slider = document.getElementById('svm-w1-slider'); + const w2Slider = document.getElementById('svm-w2-slider'); + const bSlider = document.getElementById('svm-b-slider'); + + if (w1Slider) { + w1Slider.addEventListener('input', (e) => { + state.svm.w1 = parseFloat(e.target.value); + document.getElementById('svm-w1-val').textContent = state.svm.w1.toFixed(1); + drawSVMBasic(); + }); + } + + if (w2Slider) { + w2Slider.addEventListener('input', (e) => { + state.svm.w2 = parseFloat(e.target.value); + document.getElementById('svm-w2-val').textContent = state.svm.w2.toFixed(1); + drawSVMBasic(); }); - const activeLink = document.querySelector(`[data-topic="${topicId}"]`); - if (activeLink) { - activeLink.classList.add('active'); + } + + if (bSlider) { + bSlider.addEventListener('input', (e) => { + state.svm.b = parseFloat(e.target.value); + document.getElementById('svm-b-val').textContent = state.svm.b.toFixed(1); + drawSVMBasic(); + }); + } + + drawSVMBasic(); +} + +function drawSVMBasic() { + const canvas = document.getElementById('svm-basic-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) { + console.warn('Could not get canvas context for svm-basic-canvas'); + return; + } + + const width = canvas.width = canvas.offsetWidth || 600; + const height = canvas.height = 450; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const xMin = 0, xMax = 10; + const yMin = 0, yMax = 10; + + const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * chartWidth; + const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * chartHeight; + + // Draw grid + ctx.strokeStyle = 'rgba(42, 53, 68, 0.5)'; + ctx.lineWidth = 1; + for (let i = 0; i <= 10; i++) { + const x = scaleX(i); + const y = scaleY(i); + + ctx.beginPath(); + ctx.moveTo(x, padding); + ctx.lineTo(x, height - padding); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(padding, y); + ctx.lineTo(width - padding, y); + ctx.stroke(); + } + + // Draw axes + ctx.strokeStyle = '#2a3544'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(padding, padding); + ctx.lineTo(padding, height - padding); + ctx.lineTo(width - padding, height - padding); + ctx.stroke(); + + // Draw decision boundary + const w1 = state.svm.w1; + const w2 = state.svm.w2; + const b = state.svm.b; + + if (Math.abs(w2) > 0.01) { + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 3; + ctx.beginPath(); + + const x1 = xMin; + const y1 = -(w1 * x1 + b) / w2; + const x2 = xMax; + const y2 = -(w1 * x2 + b) / w2; + + ctx.moveTo(scaleX(x1), scaleY(y1)); + ctx.lineTo(scaleX(x2), scaleY(y2)); + ctx.stroke(); + } + + // Draw data points + data.svm.forEach(point => { + const x = scaleX(point.x1); + const y = scaleY(point.x2); + const score = w1 * point.x1 + w2 * point.x2 + b; + + ctx.fillStyle = point.class === 1 ? '#7ef0d4' : '#ff8c6a'; + ctx.beginPath(); + ctx.arc(x, y, 8, 0, 2 * Math.PI); + ctx.fill(); + + ctx.strokeStyle = '#1a2332'; + ctx.lineWidth = 2; + ctx.stroke(); + + // Label + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(point.label, x, y - 15); + + // Score + ctx.font = '11px monospace'; + ctx.fillStyle = '#a9b4c2'; + ctx.fillText(score.toFixed(2), x, y + 20); + }); + + // Labels + ctx.fillStyle = '#a9b4c2'; + ctx.font = '13px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('X₁', width / 2, height - 20); + ctx.save(); + ctx.translate(20, height / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('X₂', 0, 0); + ctx.restore(); + + // Equation + ctx.fillStyle = '#7ef0d4'; + ctx.font = '14px monospace'; + ctx.textAlign = 'left'; + ctx.fillText(`w·x + b = ${w1.toFixed(1)}x₁ + ${w2.toFixed(1)}x₂ + ${b.toFixed(1)}`, padding + 10, padding + 25); +} + +function initSVMMargin() { + const canvas = document.getElementById('svm-margin-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + drawSVMMargin(); +} + +function drawSVMMargin() { + const canvas = document.getElementById('svm-margin-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) { + console.warn('Could not get canvas context for svm-margin-canvas'); + return; + } + + const width = canvas.width = canvas.offsetWidth || 600; + const height = canvas.height = 450; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const xMin = 0, xMax = 10; + const yMin = 0, yMax = 10; + + const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * chartWidth; + const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * chartHeight; + + // Use good values for visualization + const w1 = 0.5, w2 = -1, b = 5.5; + + // Draw margin lines + if (Math.abs(w2) > 0.01) { + // Positive margin line + ctx.strokeStyle = '#ff8c6a'; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + ctx.beginPath(); + let x1 = xMin, y1 = -(w1 * x1 + b - 1) / w2; + let x2 = xMax, y2 = -(w1 * x2 + b - 1) / w2; + ctx.moveTo(scaleX(x1), scaleY(y1)); + ctx.lineTo(scaleX(x2), scaleY(y2)); + ctx.stroke(); + + // Negative margin line + ctx.beginPath(); + y1 = -(w1 * x1 + b + 1) / w2; + y2 = -(w1 * x2 + b + 1) / w2; + ctx.moveTo(scaleX(x1), scaleY(y1)); + ctx.lineTo(scaleX(x2), scaleY(y2)); + ctx.stroke(); + + // Decision boundary + ctx.setLineDash([]); + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 3; + ctx.beginPath(); + y1 = -(w1 * x1 + b) / w2; + y2 = -(w1 * x2 + b) / w2; + ctx.moveTo(scaleX(x1), scaleY(y1)); + ctx.lineTo(scaleX(x2), scaleY(y2)); + ctx.stroke(); + } + + // Draw data points + data.svm.forEach(point => { + const x = scaleX(point.x1); + const y = scaleY(point.x2); + const score = w1 * point.x1 + w2 * point.x2 + b; + const isSupport = Math.abs(Math.abs(score) - 1) < 0.5; + + ctx.fillStyle = point.class === 1 ? '#7ef0d4' : '#ff8c6a'; + ctx.beginPath(); + ctx.arc(x, y, 8, 0, 2 * Math.PI); + ctx.fill(); + + // Highlight support vectors + if (isSupport) { + ctx.strokeStyle = '#7ef0d4'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(x, y, 14, 0, 2 * Math.PI); + ctx.stroke(); } - currentTopic = parseInt(topicId); + + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(point.label, x, y - 20); + }); + + // Show margin width + const wNorm = Math.sqrt(w1 * w1 + w2 * w2); + const marginWidth = 2 / wNorm; + + ctx.fillStyle = '#7ef0d4'; + ctx.font = '16px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(`Margin Width: ${marginWidth.toFixed(2)}`, padding + 10, padding + 25); + ctx.fillText('Support vectors highlighted with cyan ring', padding + 10, padding + 50); } -// ===== SCROLL OBSERVER ===== -function setupScrollObserver() { - const options = { - root: null, - rootMargin: '-100px', - threshold: 0.3 - }; +function initSVMCParameter() { + const canvas = document.getElementById('svm-c-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + + const cSlider = document.getElementById('svm-c-slider'); + if (cSlider) { + cSlider.addEventListener('input', (e) => { + const val = parseFloat(e.target.value); + state.svm.C = Math.pow(10, val); + document.getElementById('svm-c-val').textContent = state.svm.C.toFixed(state.svm.C < 10 ? 1 : 0); + drawSVMCParameter(); + }); + } + + drawSVMCParameter(); +} - const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - const topicId = entry.target.id.split('-')[1]; - updateActiveLink(topicId); - } - }); - }, options); +let svmCChart = null; + +function drawSVMCParameter() { + const canvas = document.getElementById('svm-c-canvas'); + if (!canvas) return; + + // Destroy existing chart + if (svmCChart) { + svmCChart.destroy(); + } + + const ctx = canvas.getContext('2d'); + if (!ctx) { + console.warn('Could not get canvas context for svm-c-canvas'); + return; + } + + const width = canvas.width = canvas.offsetWidth || 600; + const height = canvas.height = 450; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const xMin = 0, xMax = 10; + const yMin = 0, yMax = 10; + + const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * chartWidth; + const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * chartHeight; + + // Adjust margin based on C + const C = state.svm.C; + const marginFactor = Math.min(1, 10 / C); + const w1 = 0.5 * marginFactor, w2 = -1 * marginFactor, b = 5.5; + + // Calculate violations + let violations = 0; + data.svm.forEach(point => { + const score = w1 * point.x1 + w2 * point.x2 + b; + if (point.class * score < 1) violations++; + }); + + // Draw margin lines + if (Math.abs(w2) > 0.01) { + ctx.strokeStyle = '#ff8c6a'; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + ctx.beginPath(); + let x1 = xMin, y1 = -(w1 * x1 + b - 1) / w2; + let x2 = xMax, y2 = -(w1 * x2 + b - 1) / w2; + ctx.moveTo(scaleX(x1), scaleY(y1)); + ctx.lineTo(scaleX(x2), scaleY(y2)); + ctx.stroke(); + + ctx.beginPath(); + y1 = -(w1 * x1 + b + 1) / w2; + y2 = -(w1 * x2 + b + 1) / w2; + ctx.moveTo(scaleX(x1), scaleY(y1)); + ctx.lineTo(scaleX(x2), scaleY(y2)); + ctx.stroke(); + + ctx.setLineDash([]); + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 3; + ctx.beginPath(); + y1 = -(w1 * x1 + b) / w2; + y2 = -(w1 * x2 + b) / w2; + ctx.moveTo(scaleX(x1), scaleY(y1)); + ctx.lineTo(scaleX(x2), scaleY(y2)); + ctx.stroke(); + } + + // Draw points + data.svm.forEach(point => { + const x = scaleX(point.x1); + const y = scaleY(point.x2); + const score = w1 * point.x1 + w2 * point.x2 + b; + const violates = point.class * score < 1; + + ctx.fillStyle = point.class === 1 ? '#7ef0d4' : '#ff8c6a'; + ctx.beginPath(); + ctx.arc(x, y, 8, 0, 2 * Math.PI); + ctx.fill(); + + if (violates) { + ctx.strokeStyle = '#ff4444'; + ctx.lineWidth = 3; + ctx.stroke(); + } + }); + + // Update info + const wNorm = Math.sqrt(w1 * w1 + w2 * w2); + const marginWidth = 2 / wNorm; + const marginEl = document.getElementById('margin-width'); + const violEl = document.getElementById('violations-count'); + if (marginEl) marginEl.textContent = marginWidth.toFixed(2); + if (violEl) violEl.textContent = violations; +} - document.querySelectorAll('.topic-section').forEach(section => { - observer.observe(section); +function initSVMTraining() { + const canvas = document.getElementById('svm-train-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + + const trainBtn = document.getElementById('svm-train-btn'); + const stepBtn = document.getElementById('svm-step-btn'); + const resetBtn = document.getElementById('svm-reset-btn'); + + if (trainBtn) { + trainBtn.addEventListener('click', () => { + state.svm.training.step = 0; + state.svm.training.w = [0, 0]; + state.svm.training.b = 0; + state.svm.training.isTraining = true; + autoTrain(); + }); + } + + if (stepBtn) { + stepBtn.addEventListener('click', () => { + if (state.svm.training.step < data.svm.length) { + trainStep(); + } + }); + } + + if (resetBtn) { + resetBtn.addEventListener('click', () => { + state.svm.training.step = 0; + state.svm.training.w = [0, 0]; + state.svm.training.b = 0; + state.svm.training.isTraining = false; + updateTrainingInfo(); + drawSVMTraining(); }); + } + + drawSVMTraining(); } -// ===== CANVAS UTILITIES ===== -function clearCanvas(ctx, canvas) { - ctx.fillStyle = COLORS.background; - ctx.fillRect(0, 0, canvas.width, canvas.height); +function trainStep() { + if (state.svm.training.step >= data.svm.length) return; + + const point = data.svm[state.svm.training.step]; + const w = state.svm.training.w; + const b = state.svm.training.b; + const lr = state.svm.training.learningRate; + const C = 1; + + const score = w[0] * point.x1 + w[1] * point.x2 + b; + const violation = point.class * score < 1; + + if (violation) { + w[0] = w[0] - lr * (w[0] - C * point.class * point.x1); + w[1] = w[1] - lr * (w[1] - C * point.class * point.x2); + state.svm.training.b = b + lr * C * point.class; + } else { + w[0] = w[0] - lr * w[0]; + w[1] = w[1] - lr * w[1]; + } + + state.svm.training.step++; + updateTrainingInfo(point, violation); + drawSVMTraining(); } -function drawText(ctx, text, x, y, fontSize = 14, color = COLORS.text, align = 'center') { - ctx.fillStyle = color; - ctx.font = `${fontSize}px 'Segoe UI', sans-serif`; - ctx.textAlign = align; - ctx.fillText(text, x, y); +function autoTrain() { + if (!state.svm.training.isTraining) return; + + if (state.svm.training.step < data.svm.length) { + trainStep(); + setTimeout(autoTrain, 800); + } else { + state.svm.training.isTraining = false; + } } -function drawCircle(ctx, x, y, radius, color, filled = true) { +function updateTrainingInfo(point = null, violation = null) { + document.getElementById('train-step').textContent = state.svm.training.step; + document.getElementById('train-point').textContent = point ? `${point.label} (${point.x1}, ${point.x2})` : '-'; + document.getElementById('train-w').textContent = `${state.svm.training.w[0].toFixed(2)}, ${state.svm.training.w[1].toFixed(2)}`; + document.getElementById('train-b').textContent = state.svm.training.b.toFixed(2); + document.getElementById('train-violation').textContent = violation === null ? '-' : (violation ? 'YES ❌' : 'NO ✓'); +} + +function drawSVMTraining() { + const canvas = document.getElementById('svm-train-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) { + console.warn('Could not get canvas context for svm-train-canvas'); + return; + } + + const width = canvas.width = canvas.offsetWidth || 600; + const height = canvas.height = 450; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const xMin = 0, xMax = 10; + const yMin = 0, yMax = 10; + + const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * chartWidth; + const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * chartHeight; + + const w = state.svm.training.w; + const b = state.svm.training.b; + + // Draw boundary if weights are non-zero + if (Math.abs(w[1]) > 0.01) { + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 3; ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - if (filled) { - ctx.fillStyle = color; - ctx.fill(); - } else { - ctx.strokeStyle = color; - ctx.lineWidth = 2; - ctx.stroke(); + const x1 = xMin, y1 = -(w[0] * x1 + b) / w[1]; + const x2 = xMax, y2 = -(w[0] * x2 + b) / w[1]; + ctx.moveTo(scaleX(x1), scaleY(y1)); + ctx.lineTo(scaleX(x2), scaleY(y2)); + ctx.stroke(); + } + + // Draw points + data.svm.forEach((point, i) => { + const x = scaleX(point.x1); + const y = scaleY(point.x2); + const processed = i < state.svm.training.step; + const current = i === state.svm.training.step - 1; + + ctx.fillStyle = point.class === 1 ? '#7ef0d4' : '#ff8c6a'; + ctx.globalAlpha = processed ? 1 : 0.3; + ctx.beginPath(); + ctx.arc(x, y, 8, 0, 2 * Math.PI); + ctx.fill(); + + if (current) { + ctx.globalAlpha = 1; + ctx.strokeStyle = '#ffff00'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(x, y, 14, 0, 2 * Math.PI); + ctx.stroke(); } + + ctx.globalAlpha = 1; + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(point.label, x, y - 15); + }); +} + +let svmKernelChart = null; + +function initSVMKernel() { + const canvas = document.getElementById('svm-kernel-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + + const kernelRadios = document.querySelectorAll('input[name="kernel"]'); + kernelRadios.forEach(radio => { + radio.addEventListener('change', (e) => { + state.svm.kernel = e.target.value; + const paramGroup = document.getElementById('kernel-param-group'); + if (paramGroup) { + paramGroup.style.display = state.svm.kernel === 'linear' ? 'none' : 'block'; + } + drawSVMKernel(); + }); + }); + + const paramSlider = document.getElementById('kernel-param-slider'); + if (paramSlider) { + paramSlider.addEventListener('input', (e) => { + state.svm.kernelParam = parseFloat(e.target.value); + const paramVal = document.getElementById('kernel-param-val'); + if (paramVal) paramVal.textContent = state.svm.kernelParam.toFixed(1); + drawSVMKernel(); + }); + } + + drawSVMKernel(); +} + +function drawSVMKernel() { + const canvas = document.getElementById('svm-kernel-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 500; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + // Generate circular data + const innerPoints = []; + const outerPoints = []; + + for (let i = 0; i < 15; i++) { + const angle = (i / 15) * 2 * Math.PI; + innerPoints.push({ x: 5 + 1.5 * Math.cos(angle), y: 5 + 1.5 * Math.sin(angle), class: 1 }); + } + + for (let i = 0; i < 20; i++) { + const angle = (i / 20) * 2 * Math.PI; + const r = 3.5 + Math.random() * 0.5; + outerPoints.push({ x: 5 + r * Math.cos(angle), y: 5 + r * Math.sin(angle), class: -1 }); + } + + const allPoints = [...innerPoints, ...outerPoints]; + + const xMin = 0, xMax = 10; + const yMin = 0, yMax = 10; + + const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * chartWidth; + const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * chartHeight; + + // Draw decision boundary based on kernel + if (state.svm.kernel === 'linear') { + // Linear can't separate circular data well + ctx.strokeStyle = 'rgba(106, 169, 255, 0.5)'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(scaleX(2), scaleY(2)); + ctx.lineTo(scaleX(8), scaleY(8)); + ctx.stroke(); + } else if (state.svm.kernel === 'polynomial' || state.svm.kernel === 'rbf') { + // Draw circular boundary + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 3; + ctx.beginPath(); + const radius = state.svm.kernel === 'polynomial' ? 2.5 : 2.3 + state.svm.kernelParam * 0.1; + ctx.arc(scaleX(5), scaleY(5), radius * (chartWidth / 10), 0, 2 * Math.PI); + ctx.stroke(); + } + + // Draw points + allPoints.forEach(point => { + const x = scaleX(point.x); + const y = scaleY(point.y); + + ctx.fillStyle = point.class === 1 ? '#7ef0d4' : '#ff8c6a'; + ctx.beginPath(); + ctx.arc(x, y, 5, 0, 2 * Math.PI); + ctx.fill(); + }); + + // Draw kernel info + ctx.fillStyle = '#7ef0d4'; + ctx.font = '16px sans-serif'; + ctx.textAlign = 'left'; + const kernelName = state.svm.kernel === 'linear' ? 'Linear Kernel' : + state.svm.kernel === 'polynomial' ? 'Polynomial Kernel' : 'RBF Kernel'; + ctx.fillText(kernelName, padding + 10, padding + 25); + + if (state.svm.kernel === 'linear') { + ctx.font = '13px sans-serif'; + ctx.fillStyle = '#ff8c6a'; + ctx.fillText('❌ Linear kernel cannot separate circular data!', padding + 10, padding + 50); + } else { + ctx.font = '13px sans-serif'; + ctx.fillStyle = '#7ef0d4'; + ctx.fillText('✓ Non-linear kernel successfully separates the data', padding + 10, padding + 50); + } +} + +// Logistic Regression Visualizations +function initLogistic() { + initSigmoid(); + initLogisticClassification(); } -function drawLine(ctx, x1, y1, x2, y2, color = COLORS.text, width = 1) { +function initSigmoid() { + const canvas = document.getElementById('sigmoid-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + drawSigmoid(); +} + +function drawSigmoid() { + const canvas = document.getElementById('sigmoid-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 350; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const zMin = -10, zMax = 10; + const scaleX = (z) => padding + ((z - zMin) / (zMax - zMin)) * chartWidth; + const scaleY = (sig) => height - padding - sig * chartHeight; + + // Draw grid + ctx.strokeStyle = 'rgba(42, 53, 68, 0.5)'; + ctx.lineWidth = 1; + for (let i = 0; i <= 10; i++) { + const x = padding + (chartWidth / 10) * i; + ctx.beginPath(); + ctx.moveTo(x, padding); + ctx.lineTo(x, height - padding); + ctx.stroke(); + + const y = padding + (chartHeight / 10) * i; ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.strokeStyle = color; - ctx.lineWidth = width; + ctx.moveTo(padding, y); + ctx.lineTo(width - padding, y); ctx.stroke(); + } + + // Draw axes + ctx.strokeStyle = '#2a3544'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(padding, padding); + ctx.lineTo(padding, height - padding); + ctx.lineTo(width - padding, height - padding); + ctx.stroke(); + + // Draw threshold line at 0.5 + ctx.strokeStyle = '#ff8c6a'; + ctx.lineWidth = 1; + ctx.setLineDash([5, 5]); + ctx.beginPath(); + ctx.moveTo(padding, scaleY(0.5)); + ctx.lineTo(width - padding, scaleY(0.5)); + ctx.stroke(); + ctx.setLineDash([]); + + // Draw sigmoid curve + ctx.strokeStyle = '#7ef0d4'; + ctx.lineWidth = 3; + ctx.beginPath(); + for (let z = zMin; z <= zMax; z += 0.1) { + const sig = 1 / (1 + Math.exp(-z)); + const x = scaleX(z); + const y = scaleY(sig); + if (z === zMin) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.stroke(); + + // Labels + ctx.fillStyle = '#a9b4c2'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('z (input)', width / 2, height - 20); + ctx.save(); + ctx.translate(20, height / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('σ(z) probability', 0, 0); + ctx.restore(); + + // Annotations + ctx.fillStyle = '#7ef0d4'; + ctx.textAlign = 'left'; + ctx.fillText('σ(z) = 1/(1+e⁻ᶻ)', padding + 10, padding + 25); + ctx.fillStyle = '#ff8c6a'; + ctx.fillText('Threshold = 0.5', padding + 10, scaleY(0.5) - 10); +} + +function initLogisticClassification() { + const canvas = document.getElementById('logistic-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + drawLogisticClassification(); +} + +function drawLogisticClassification() { + const canvas = document.getElementById('logistic-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const hMin = 140, hMax = 210; + const scaleX = (h) => padding + ((h - hMin) / (hMax - hMin)) * chartWidth; + const scaleY = (p) => height - padding - p * chartHeight; + + // Draw sigmoid curve + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 3; + ctx.beginPath(); + for (let h = hMin; h <= hMax; h += 1) { + const z = (h - 170) / 10; // Simple linear transformation + const p = 1 / (1 + Math.exp(-z)); + const x = scaleX(h); + const y = scaleY(p); + if (h === hMin) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.stroke(); + + // Draw threshold line + ctx.strokeStyle = '#ff8c6a'; + ctx.setLineDash([5, 5]); + ctx.beginPath(); + ctx.moveTo(padding, scaleY(0.5)); + ctx.lineTo(width - padding, scaleY(0.5)); + ctx.stroke(); + ctx.setLineDash([]); + + // Draw data points + data.logistic.forEach(point => { + const x = scaleX(point.height); + const y = scaleY(point.prob); + + ctx.fillStyle = point.label === 1 ? '#7ef0d4' : '#ff8c6a'; + ctx.beginPath(); + ctx.arc(x, y, 6, 0, 2 * Math.PI); + ctx.fill(); + + // Label + ctx.fillStyle = '#e8eef6'; + ctx.font = '11px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(point.height, x, height - padding + 20); + }); + + // Labels + ctx.fillStyle = '#a9b4c2'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Height (cm)', width / 2, height - 20); + ctx.save(); + ctx.translate(20, height / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('P(Tall)', 0, 0); + ctx.restore(); +} + +// KNN Visualization +let knnState = { testPoint: { x: 2.5, y: 2.5 }, k: 3, distanceMetric: 'euclidean', dragging: false }; + +function initKNN() { + const canvas = document.getElementById('knn-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + + const kSlider = document.getElementById('knn-k-slider'); + if (kSlider) { + kSlider.addEventListener('input', (e) => { + knnState.k = parseInt(e.target.value); + document.getElementById('knn-k-val').textContent = knnState.k; + drawKNN(); + }); + } + + const distanceRadios = document.querySelectorAll('input[name="knn-distance"]'); + distanceRadios.forEach(radio => { + radio.addEventListener('change', (e) => { + knnState.distanceMetric = e.target.value; + drawKNN(); + }); + }); + + canvas.addEventListener('mousedown', startDragKNN); + canvas.addEventListener('mousemove', dragKNN); + canvas.addEventListener('mouseup', stopDragKNN); + + drawKNN(); +} + +function startDragKNN(e) { + const canvas = document.getElementById('knn-canvas'); + const rect = canvas.getBoundingClientRect(); + const mx = e.clientX - rect.left; + const my = e.clientY - rect.top; + + const padding = 60; + const chartWidth = canvas.width - 2 * padding; + const chartHeight = canvas.height - 2 * padding; + + const tx = padding + (knnState.testPoint.x / 6) * chartWidth; + const ty = canvas.height - padding - (knnState.testPoint.y / 6) * chartHeight; + + if (Math.abs(mx - tx) < 15 && Math.abs(my - ty) < 15) { + knnState.dragging = true; + } +} + +function dragKNN(e) { + if (!knnState.dragging) return; + + const canvas = document.getElementById('knn-canvas'); + const rect = canvas.getBoundingClientRect(); + const mx = e.clientX - rect.left; + const my = e.clientY - rect.top; + + const padding = 60; + const chartWidth = canvas.width - 2 * padding; + const chartHeight = canvas.height - 2 * padding; + + knnState.testPoint.x = Math.max(0, Math.min(6, ((mx - padding) / chartWidth) * 6)); + knnState.testPoint.y = Math.max(0, Math.min(6, ((canvas.height - padding - my) / chartHeight) * 6)); + + drawKNN(); +} + +function stopDragKNN() { + knnState.dragging = false; } -function drawRect(ctx, x, y, width, height, color, filled = true) { - if (filled) { - ctx.fillStyle = color; - ctx.fillRect(x, y, width, height); +function drawKNN() { + const canvas = document.getElementById('knn-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 450; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const scaleX = (x) => padding + (x / 6) * chartWidth; + const scaleY = (y) => height - padding - (y / 6) * chartHeight; + + // Calculate distances + const distances = data.knn.map(point => { + let d; + if (knnState.distanceMetric === 'euclidean') { + d = Math.sqrt(Math.pow(point.x - knnState.testPoint.x, 2) + Math.pow(point.y - knnState.testPoint.y, 2)); } else { - ctx.strokeStyle = color; - ctx.lineWidth = 2; - ctx.strokeRect(x, y, width, height); + d = Math.abs(point.x - knnState.testPoint.x) + Math.abs(point.y - knnState.testPoint.y); + } + return { ...point, distance: d }; + }); + + distances.sort((a, b) => a.distance - b.distance); + const kNearest = distances.slice(0, knnState.k); + + // Count votes + const votes = {}; + kNearest.forEach(p => { + votes[p.class] = (votes[p.class] || 0) + 1; + }); + const prediction = Object.keys(votes).reduce((a, b) => votes[a] > votes[b] ? a : b); + + // Draw lines to K nearest + kNearest.forEach(point => { + ctx.strokeStyle = 'rgba(126, 240, 212, 0.3)'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(scaleX(knnState.testPoint.x), scaleY(knnState.testPoint.y)); + ctx.lineTo(scaleX(point.x), scaleY(point.y)); + ctx.stroke(); + }); + + // Draw training points + distances.forEach(point => { + const x = scaleX(point.x); + const y = scaleY(point.y); + const isNearest = kNearest.includes(point); + + ctx.fillStyle = point.class === 'orange' ? '#ff8c6a' : '#ffeb3b'; + ctx.globalAlpha = isNearest ? 1 : 0.5; + ctx.beginPath(); + ctx.arc(x, y, 8, 0, 2 * Math.PI); + ctx.fill(); + + if (isNearest) { + ctx.strokeStyle = '#7ef0d4'; + ctx.lineWidth = 2; + ctx.globalAlpha = 1; + ctx.beginPath(); + ctx.arc(x, y, 12, 0, 2 * Math.PI); + ctx.stroke(); } + ctx.globalAlpha = 1; + }); + + // Draw test point + const tx = scaleX(knnState.testPoint.x); + const ty = scaleY(knnState.testPoint.y); + ctx.fillStyle = prediction === 'orange' ? '#ff8c6a' : '#ffeb3b'; + ctx.beginPath(); + ctx.arc(tx, ty, 12, 0, 2 * Math.PI); + ctx.fill(); + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 3; + ctx.stroke(); + + // Info + ctx.fillStyle = '#7ef0d4'; + ctx.font = '14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(`K=${knnState.k} | Prediction: ${prediction}`, padding + 10, padding + 25); + ctx.fillText(`Votes: Orange=${votes.orange || 0}, Yellow=${votes.yellow || 0}`, padding + 10, padding + 50); } -// ===== STATISTICAL CALCULATIONS ===== -function calculateMean(data) { - return data.reduce((sum, val) => sum + val, 0) / data.length; +// Model Evaluation +function initModelEvaluation() { + initConfusionMatrix(); + initROC(); + initR2(); } -function calculateMedian(data) { - const sorted = [...data].sort((a, b) => a - b); - const mid = Math.floor(sorted.length / 2); - return sorted.length % 2 === 0 - ? (sorted[mid - 1] + sorted[mid]) / 2 - : sorted[mid]; +function initConfusionMatrix() { + const canvas = document.getElementById('confusion-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + drawConfusionMatrix(); } -function calculateMode(data) { - const frequency = {}; - let maxFreq = 0; - - data.forEach(val => { - frequency[val] = (frequency[val] || 0) + 1; - maxFreq = Math.max(maxFreq, frequency[val]); +function drawConfusionMatrix() { + const canvas = document.getElementById('confusion-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 300; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const size = Math.min(width, height) - 100; + const cellSize = size / 2; + const startX = (width - size) / 2; + const startY = 50; + + const cm = { tp: 600, fp: 100, fn: 300, tn: 900 }; + + // Draw cells + const cells = [ + { x: startX, y: startY, val: cm.tp, label: 'TP', color: '#7ef0d4' }, + { x: startX + cellSize, y: startY, val: cm.fn, label: 'FN', color: '#ff8c6a' }, + { x: startX, y: startY + cellSize, val: cm.fp, label: 'FP', color: '#ff8c6a' }, + { x: startX + cellSize, y: startY + cellSize, val: cm.tn, label: 'TN', color: '#7ef0d4' } + ]; + + cells.forEach(cell => { + ctx.fillStyle = cell.color + '22'; + ctx.fillRect(cell.x, cell.y, cellSize, cellSize); + ctx.strokeStyle = cell.color; + ctx.lineWidth = 2; + ctx.strokeRect(cell.x, cell.y, cellSize, cellSize); + + ctx.fillStyle = cell.color; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(cell.label, cell.x + cellSize / 2, cell.y + cellSize / 2 - 10); + ctx.font = 'bold 32px sans-serif'; + ctx.fillText(cell.val, cell.x + cellSize / 2, cell.y + cellSize / 2 + 25); + }); + + // Labels + ctx.fillStyle = '#a9b4c2'; + ctx.font = '14px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Predicted Positive', startX + cellSize / 2, startY - 15); + ctx.fillText('Predicted Negative', startX + cellSize * 1.5, startY - 15); + ctx.save(); + ctx.translate(startX - 30, startY + cellSize / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('Actual Positive', 0, 0); + ctx.restore(); + ctx.save(); + ctx.translate(startX - 30, startY + cellSize * 1.5); + ctx.rotate(-Math.PI / 2); + ctx.fillText('Actual Negative', 0, 0); + ctx.restore(); +} + +let rocState = { threshold: 0.5 }; + +function initROC() { + const canvas = document.getElementById('roc-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + + const slider = document.getElementById('roc-threshold-slider'); + if (slider) { + slider.addEventListener('input', (e) => { + rocState.threshold = parseFloat(e.target.value); + document.getElementById('roc-threshold-val').textContent = rocState.threshold.toFixed(1); + drawROC(); }); - - if (maxFreq === 1) return 'None'; - - const modes = Object.keys(frequency).filter(key => frequency[key] === maxFreq); - return modes.join(', '); + } + + drawROC(); } -function calculateVariance(data, isSample = true) { - const mean = calculateMean(data); - const squaredDiffs = data.map(val => Math.pow(val - mean, 2)); - const divisor = isSample ? data.length - 1 : data.length; - return squaredDiffs.reduce((sum, val) => sum + val, 0) / divisor; +function drawROC() { + const canvas = document.getElementById('roc-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 450; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartSize = Math.min(width - 2 * padding, height - 2 * padding); + const chartX = (width - chartSize) / 2; + const chartY = (height - chartSize) / 2; + + // Calculate ROC points + const rocPoints = []; + for (let t = 0; t <= 1; t += 0.1) { + let tp = 0, fp = 0, tn = 0, fn = 0; + data.roc.forEach(e => { + const pred = e.score >= t ? 1 : 0; + if (e.true_label === 1 && pred === 1) tp++; + else if (e.true_label === 0 && pred === 1) fp++; + else if (e.true_label === 1 && pred === 0) fn++; + else tn++; + }); + const tpr = tp / (tp + fn) || 0; + const fpr = fp / (fp + tn) || 0; + rocPoints.push({ t, tpr, fpr }); + } + + // Current threshold point + let tp = 0, fp = 0, tn = 0, fn = 0; + data.roc.forEach(e => { + const pred = e.score >= rocState.threshold ? 1 : 0; + if (e.true_label === 1 && pred === 1) tp++; + else if (e.true_label === 0 && pred === 1) fp++; + else if (e.true_label === 1 && pred === 0) fn++; + else tn++; + }); + const tpr = tp / (tp + fn) || 0; + const fpr = fp / (fp + tn) || 0; + + // Draw diagonal (random) + ctx.strokeStyle = 'rgba(255, 140, 106, 0.5)'; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + ctx.beginPath(); + ctx.moveTo(chartX, chartY + chartSize); + ctx.lineTo(chartX + chartSize, chartY); + ctx.stroke(); + ctx.setLineDash([]); + + // Draw ROC curve + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 3; + ctx.beginPath(); + rocPoints.forEach((p, i) => { + const x = chartX + p.fpr * chartSize; + const y = chartY + chartSize - p.tpr * chartSize; + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + }); + ctx.stroke(); + + // Draw current point + const cx = chartX + fpr * chartSize; + const cy = chartY + chartSize - tpr * chartSize; + ctx.fillStyle = '#7ef0d4'; + ctx.beginPath(); + ctx.arc(cx, cy, 8, 0, 2 * Math.PI); + ctx.fill(); + + // Draw axes + ctx.strokeStyle = '#2a3544'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.rect(chartX, chartY, chartSize, chartSize); + ctx.stroke(); + + // Labels + ctx.fillStyle = '#a9b4c2'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('FPR (False Positive Rate)', width / 2, height - 20); + ctx.save(); + ctx.translate(20, height / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('TPR (True Positive Rate)', 0, 0); + ctx.restore(); + + // Info + ctx.fillStyle = '#7ef0d4'; + ctx.font = '14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(`TPR: ${tpr.toFixed(2)} | FPR: ${fpr.toFixed(2)}`, chartX + 10, chartY + 25); + ctx.fillText(`TP=${tp} FP=${fp} TN=${tn} FN=${fn}`, chartX + 10, chartY + 50); } -function calculateStdDev(data, isSample = true) { - return Math.sqrt(calculateVariance(data, isSample)); +function initR2() { + const canvas = document.getElementById('r2-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + drawR2(); } -function calculateQuartiles(data) { - const sorted = [...data].sort((a, b) => a - b); - const q2 = calculateMedian(sorted); - const midIndex = Math.floor(sorted.length / 2); - - const lowerHalf = sorted.length % 2 === 0 - ? sorted.slice(0, midIndex) - : sorted.slice(0, midIndex); - const upperHalf = sorted.length % 2 === 0 - ? sorted.slice(midIndex) - : sorted.slice(midIndex + 1); - - const q1 = calculateMedian(lowerHalf); - const q3 = calculateMedian(upperHalf); +function drawR2() { + const canvas = document.getElementById('r2-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 350; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + // Dummy R² data + const r2data = [ + { x: 150, y: 50, pred: 52 }, + { x: 160, y: 60, pred: 61 }, + { x: 170, y: 70, pred: 69 }, + { x: 180, y: 80, pred: 78 }, + { x: 190, y: 90, pred: 87 } + ]; + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const xMin = 140, xMax = 200, yMin = 40, yMax = 100; + const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * chartWidth; + const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * chartHeight; + + // Mean + const mean = r2data.reduce((sum, p) => sum + p.y, 0) / r2data.length; + + // Draw mean line + ctx.strokeStyle = '#ff8c6a'; + ctx.setLineDash([5, 5]); + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(padding, scaleY(mean)); + ctx.lineTo(width - padding, scaleY(mean)); + ctx.stroke(); + ctx.setLineDash([]); + + // Draw regression line + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(scaleX(xMin), scaleY(40)); + ctx.lineTo(scaleX(xMax), scaleY(95)); + ctx.stroke(); + + // Draw points + r2data.forEach(p => { + // Residual line + ctx.strokeStyle = 'rgba(126, 240, 212, 0.3)'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(scaleX(p.x), scaleY(p.y)); + ctx.lineTo(scaleX(p.x), scaleY(p.pred)); + ctx.stroke(); - return { q1, q2, q3 }; + // Actual point + ctx.fillStyle = '#7ef0d4'; + ctx.beginPath(); + ctx.arc(scaleX(p.x), scaleY(p.y), 6, 0, 2 * Math.PI); + ctx.fill(); + }); + + // Calculate R² + let ssRes = 0, ssTot = 0; + r2data.forEach(p => { + ssRes += Math.pow(p.y - p.pred, 2); + ssTot += Math.pow(p.y - mean, 2); + }); + const r2 = 1 - (ssRes / ssTot); + + // Info + ctx.fillStyle = '#7ef0d4'; + ctx.font = '16px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(`R² = ${r2.toFixed(3)}`, padding + 10, padding + 25); + ctx.fillText(`Model explains ${(r2 * 100).toFixed(1)}% of variance`, padding + 10, padding + 50); } -function calculateIQR(data) { - const { q1, q3 } = calculateQuartiles(data); - const iqr = q3 - q1; - const lowerFence = q1 - 1.5 * iqr; - const upperFence = q3 + 1.5 * iqr; - - return { q1, q3, iqr, lowerFence, upperFence }; +// Regularization +let regState = { lambda: 0.1 }; + +function initRegularization() { + const canvas = document.getElementById('regularization-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + + const slider = document.getElementById('reg-lambda-slider'); + if (slider) { + slider.addEventListener('input', (e) => { + regState.lambda = parseFloat(e.target.value); + document.getElementById('reg-lambda-val').textContent = regState.lambda.toFixed(1); + drawRegularization(); + }); + } + + drawRegularization(); } -function detectOutliers(data) { - const { lowerFence, upperFence } = calculateIQR(data); - return data.filter(val => val < lowerFence || val > upperFence); +function drawRegularization() { + const canvas = document.getElementById('regularization-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const features = ['x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9', 'x10']; + const vanilla = [100, 200, 300, 50, 150, 250, 80, 120, 90, 180]; + + // Simulate L1 and L2 effects + const l1 = vanilla.map(v => Math.abs(v) > 50 / regState.lambda ? v * (1 - regState.lambda * 0.5) : 0); + const l2 = vanilla.map(v => v / (1 + regState.lambda)); + + const barWidth = chartWidth / (features.length * 3.5); + const maxVal = Math.max(...vanilla); + + features.forEach((f, i) => { + const x = padding + (i * chartWidth / features.length); + + // Vanilla + const h1 = (vanilla[i] / maxVal) * chartHeight * 0.8; + ctx.fillStyle = '#a9b4c2'; + ctx.fillRect(x, height - padding - h1, barWidth, h1); + + // L1 + const h2 = (l1[i] / maxVal) * chartHeight * 0.8; + ctx.fillStyle = '#ff8c6a'; + ctx.fillRect(x + barWidth * 1.2, height - padding - h2, barWidth, h2); + + // L2 + const h3 = (l2[i] / maxVal) * chartHeight * 0.8; + ctx.fillStyle = '#6aa9ff'; + ctx.fillRect(x + barWidth * 2.4, height - padding - h3, barWidth, h3); + + // Feature label + ctx.fillStyle = '#a9b4c2'; + ctx.font = '11px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(f, x + barWidth * 1.5, height - padding + 20); + }); + + // Legend + const legendY = padding + 20; + ctx.fillStyle = '#a9b4c2'; + ctx.fillRect(padding + 10, legendY, 15, 15); + ctx.fillStyle = '#e8eef6'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Vanilla', padding + 30, legendY + 12); + + ctx.fillStyle = '#ff8c6a'; + ctx.fillRect(padding + 100, legendY, 15, 15); + ctx.fillStyle = '#e8eef6'; + ctx.fillText('L1 (Lasso)', padding + 120, legendY + 12); + + ctx.fillStyle = '#6aa9ff'; + ctx.fillRect(padding + 210, legendY, 15, 15); + ctx.fillStyle = '#e8eef6'; + ctx.fillText('L2 (Ridge)', padding + 230, legendY + 12); } -function calculateCovariance(x, y) { - const meanX = calculateMean(x); - const meanY = calculateMean(y); - let sum = 0; +// Bias-Variance +function initBiasVariance() { + const canvas = document.getElementById('bias-variance-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + drawBiasVariance(); + + const canvas2 = document.getElementById('complexity-canvas'); + if (canvas2 && !canvas2.dataset.initialized) { + canvas2.dataset.initialized = 'true'; + drawComplexityCurve(); + } +} + +function drawBiasVariance() { + const canvas = document.getElementById('bias-variance-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const sectionWidth = width / 3; + const padding = 40; + const chartHeight = height - 2 * padding; + + // Generate curved data + const trueData = []; + for (let x = 0; x <= 10; x += 0.5) { + trueData.push({ x, y: 50 + 30 * Math.sin(x / 2) }); + } + + // Draw three scenarios + const scenarios = [ + { title: 'High Bias\n(Underfit)', color: '#ff8c6a', degree: 1 }, + { title: 'Good Fit', color: '#7ef0d4', degree: 2 }, + { title: 'High Variance\n(Overfit)', color: '#ff8c6a', degree: 8 } + ]; + + scenarios.forEach((scenario, idx) => { + const offsetX = idx * sectionWidth; + const scaleX = (x) => offsetX + padding + (x / 10) * (sectionWidth - 2 * padding); + const scaleY = (y) => padding + chartHeight - ((y - 20) / 80) * chartHeight; + + // Draw true curve + ctx.strokeStyle = 'rgba(106, 169, 255, 0.3)'; + ctx.lineWidth = 2; + ctx.beginPath(); + trueData.forEach((p, i) => { + if (i === 0) ctx.moveTo(scaleX(p.x), scaleY(p.y)); + else ctx.lineTo(scaleX(p.x), scaleY(p.y)); + }); + ctx.stroke(); - for (let i = 0; i < x.length; i++) { - sum += (x[i] - meanX) * (y[i] - meanY); + // Draw model fit + ctx.strokeStyle = scenario.color; + ctx.lineWidth = 3; + ctx.beginPath(); + if (scenario.degree === 1) { + // Straight line + ctx.moveTo(scaleX(0), scaleY(50)); + ctx.lineTo(scaleX(10), scaleY(65)); + } else if (scenario.degree === 2) { + // Good fit + trueData.forEach((p, i) => { + const noise = (Math.random() - 0.5) * 3; + if (i === 0) ctx.moveTo(scaleX(p.x), scaleY(p.y + noise)); + else ctx.lineTo(scaleX(p.x), scaleY(p.y + noise)); + }); + } else { + // Wiggly overfit + for (let x = 0; x <= 10; x += 0.2) { + const y = 50 + 30 * Math.sin(x / 2) + 15 * Math.sin(x * 2); + if (x === 0) ctx.moveTo(scaleX(x), scaleY(y)); + else ctx.lineTo(scaleX(x), scaleY(y)); + } } + ctx.stroke(); - return sum / (x.length - 1); + // Title + ctx.fillStyle = scenario.color; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'center'; + const lines = scenario.title.split('\n'); + lines.forEach((line, i) => { + ctx.fillText(line, offsetX + sectionWidth / 2, 20 + i * 18); + }); + }); } -function calculateCorrelation(x, y) { - const cov = calculateCovariance(x, y); - const stdX = calculateStdDev(x); - const stdY = calculateStdDev(y); - return cov / (stdX * stdY); +function drawComplexityCurve() { + const canvas = document.getElementById('complexity-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 350; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const scaleX = (x) => padding + (x / 10) * chartWidth; + const scaleY = (y) => padding + chartHeight - (y / 100) * chartHeight; + + // Draw curves + ctx.strokeStyle = '#ff8c6a'; + ctx.lineWidth = 3; + ctx.beginPath(); + for (let x = 0; x <= 10; x += 0.1) { + const trainError = 80 * Math.exp(-x / 2) + 5; + if (x === 0) ctx.moveTo(scaleX(x), scaleY(trainError)); + else ctx.lineTo(scaleX(x), scaleY(trainError)); + } + ctx.stroke(); + + ctx.strokeStyle = '#6aa9ff'; + ctx.beginPath(); + for (let x = 0; x <= 10; x += 0.1) { + const testError = 80 * Math.exp(-x / 2) + 5 + 15 * (x / 10) ** 2; + if (x === 0) ctx.moveTo(scaleX(x), scaleY(testError)); + else ctx.lineTo(scaleX(x), scaleY(testError)); + } + ctx.stroke(); + + // Sweet spot + ctx.fillStyle = '#7ef0d4'; + ctx.beginPath(); + ctx.arc(scaleX(5), scaleY(18), 8, 0, 2 * Math.PI); + ctx.fill(); + + // Legend + ctx.fillStyle = '#ff8c6a'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Training Error', padding + 10, padding + 20); + ctx.fillStyle = '#6aa9ff'; + ctx.fillText('Test Error', padding + 10, padding + 40); + ctx.fillStyle = '#7ef0d4'; + ctx.fillText('● Sweet Spot', padding + 10, padding + 60); + + // Labels + ctx.fillStyle = '#a9b4c2'; + ctx.textAlign = 'center'; + ctx.fillText('Model Complexity →', width / 2, height - 20); + ctx.save(); + ctx.translate(20, height / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('Error', 0, 0); + ctx.restore(); } -// ===== VISUALIZATION FUNCTIONS ===== +// Cross-Validation +function initCrossValidation() { + const canvas = document.getElementById('cv-canvas'); + if (!canvas || canvas.dataset.initialized) return; + canvas.dataset.initialized = 'true'; + drawCrossValidation(); +} -// Population vs Sample Visualization -function initPopulationSampleViz() { - const canvas = document.getElementById('populationSampleCanvas'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let population = []; - let sample = []; - let sampleSize = 30; - - // Initialize population - for (let i = 0; i < 200; i++) { - population.push({ - x: Math.random() * (canvas.width - 40) + 20, - y: Math.random() * (canvas.height - 40) + 20, - inSample: false - }); - } - - function draw() { - clearCanvas(ctx, canvas); - - // Draw title - drawText(ctx, 'Population (All dots) vs Sample (Highlighted)', canvas.width / 2, 30, 16, COLORS.cyan); - - // Draw population - population.forEach(point => { - const color = point.inSample ? COLORS.orange : COLORS.primary; - const radius = point.inSample ? 6 : 4; - drawCircle(ctx, point.x, point.y, radius, color); - }); - - // Draw statistics - const popCount = population.length; - const sampleCount = population.filter(p => p.inSample).length; - drawText(ctx, `Population Size: N = ${popCount}`, 150, canvas.height - 20, 14, COLORS.text, 'center'); - drawText(ctx, `Sample Size: n = ${sampleCount}`, canvas.width - 150, canvas.height - 20, 14, COLORS.orange, 'center'); - } - - function takeSample() { - // Reset all - population.forEach(p => p.inSample = false); - - // Randomly select sample - const shuffled = [...population].sort(() => Math.random() - 0.5); - for (let i = 0; i < Math.min(sampleSize, population.length); i++) { - shuffled[i].inSample = true; - } - - draw(); - } - - // Event listeners - const sampleBtn = document.getElementById('sampleBtn'); - const resetBtn = document.getElementById('resetPopBtn'); - const sizeSlider = document.getElementById('sampleSizeSlider'); - const sizeLabel = document.getElementById('sampleSizeLabel'); - - if (sampleBtn) { - sampleBtn.addEventListener('click', takeSample); - } - - if (resetBtn) { - resetBtn.addEventListener('click', () => { - population.forEach(p => p.inSample = false); - draw(); - }); - } - - if (sizeSlider) { - sizeSlider.addEventListener('input', (e) => { - sampleSize = parseInt(e.target.value); - if (sizeLabel) { - sizeLabel.textContent = sampleSize; - } - }); +function drawCrossValidation() { + const canvas = document.getElementById('cv-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const blockSize = 50; + const gap = 10; + const numBlocks = 12; + const k = 3; + const blocksPerFold = numBlocks / k; + + const startX = (width - (numBlocks * blockSize + (numBlocks - 1) * gap)) / 2; + + const folds = [0.96, 0.84, 0.90]; + + for (let fold = 0; fold < k; fold++) { + const offsetY = 80 + fold * 120; + + // Fold label + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'right'; + ctx.fillText(`Fold ${fold + 1}:`, startX - 20, offsetY + blockSize / 2 + 5); + + // Draw blocks + for (let i = 0; i < numBlocks; i++) { + const x = startX + i * (blockSize + gap); + const isFold = i >= fold * blocksPerFold && i < (fold + 1) * blocksPerFold; + + ctx.fillStyle = isFold ? '#6aa9ff' : '#7ef0d4'; + ctx.fillRect(x, offsetY, blockSize, blockSize); + + // Label + ctx.fillStyle = '#1a2332'; + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(String.fromCharCode(65 + i), x + blockSize / 2, offsetY + blockSize / 2 + 5); } - draw(); + // Accuracy + ctx.fillStyle = '#7ef0d4'; + ctx.font = '14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(`Acc: ${folds[fold].toFixed(2)}`, startX + numBlocks * (blockSize + gap) + 20, offsetY + blockSize / 2 + 5); + } + + // Legend + ctx.fillStyle = '#6aa9ff'; + ctx.fillRect(startX, 30, 30, 20); + ctx.fillStyle = '#e8eef6'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Test Set', startX + 40, 45); + + ctx.fillStyle = '#7ef0d4'; + ctx.fillRect(startX + 120, 30, 30, 20); + ctx.fillText('Training Set', startX + 160, 45); + + // Final result + const mean = folds.reduce((a, b) => a + b) / folds.length; + const std = Math.sqrt(folds.reduce((sum, x) => sum + Math.pow(x - mean, 2), 0) / folds.length); + + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(`Final Score: ${mean.toFixed(2)} ± ${std.toFixed(3)}`, width / 2, height - 20); } -// Central Tendency Visualization -function initCentralTendencyViz() { - const canvas = document.getElementById('centralTendencyCanvas'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let data = [10, 20, 30, 40, 50]; - - function parseInput(input) { - return input.split(',').map(s => parseFloat(s.trim())).filter(n => !isNaN(n)); - } - - function draw() { - clearCanvas(ctx, canvas); - - if (data.length === 0) { - drawText(ctx, 'Please enter valid numbers', canvas.width / 2, canvas.height / 2, 16, COLORS.orange); - return; - } - - const sorted = [...data].sort((a, b) => a - b); - const min = Math.min(...sorted); - const max = Math.max(...sorted); - const range = max - min || 1; - const padding = 80; - const width = canvas.width - 2 * padding; - - // Calculate statistics - const mean = calculateMean(data); - const median = calculateMedian(data); - const mode = calculateMode(data); - - // Update results display - document.getElementById('meanResult').textContent = mean.toFixed(2); - document.getElementById('medianResult').textContent = median.toFixed(2); - document.getElementById('modeResult').textContent = mode; - - // Draw axis - const axisY = canvas.height / 2; - drawLine(ctx, padding, axisY, canvas.width - padding, axisY, COLORS.text, 2); - - // Draw data points - sorted.forEach((val, idx) => { - const x = padding + ((val - min) / range) * width; - drawCircle(ctx, x, axisY, 8, COLORS.primary); - drawText(ctx, val.toString(), x, axisY + 30, 12, COLORS.text); - }); - - // Draw mean - const meanX = padding + ((mean - min) / range) * width; - drawLine(ctx, meanX, axisY - 60, meanX, axisY + 60, COLORS.cyan, 3); - drawText(ctx, `Mean: ${mean.toFixed(2)}`, meanX, axisY - 70, 14, COLORS.cyan); - - // Draw median - const medianX = padding + ((median - min) / range) * width; - drawLine(ctx, medianX, axisY - 50, medianX, axisY + 50, COLORS.orange, 2); - drawText(ctx, `Median: ${median.toFixed(2)}`, medianX, axisY - 55, 12, COLORS.orange); - } - - // Event listeners - const input = document.getElementById('centralTendencyInput'); - const calcBtn = document.getElementById('calculateCentralBtn'); - const randomBtn = document.getElementById('randomDataBtn'); - - if (calcBtn && input) { - calcBtn.addEventListener('click', () => { - data = parseInput(input.value); - draw(); - }); - } - - if (randomBtn && input) { - randomBtn.addEventListener('click', () => { - data = Array.from({ length: 10 }, () => Math.floor(Math.random() * 100)); - input.value = data.join(', '); - draw(); - }); - } +// Preprocessing +function initPreprocessing() { + const canvas = document.getElementById('scaling-canvas'); + if (canvas && !canvas.dataset.initialized) { + canvas.dataset.initialized = 'true'; + drawScaling(); + } + + const canvas2 = document.getElementById('pipeline-canvas'); + if (canvas2 && !canvas2.dataset.initialized) { + canvas2.dataset.initialized = 'true'; + drawPipeline(); + } +} + +function drawScaling() { + const canvas = document.getElementById('scaling-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 350; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const before = [10, 20, 30, 40, 50]; + const standard = [-1.26, -0.63, 0, 0.63, 1.26]; + const minmax = [0, 0.25, 0.5, 0.75, 1.0]; + + const sectionWidth = width / 3; + const padding = 40; + const barWidth = 30; + + const datasets = [ + { data: before, title: 'Original', maxVal: 60 }, + { data: standard, title: 'StandardScaler', maxVal: 2 }, + { data: minmax, title: 'MinMaxScaler', maxVal: 1.2 } + ]; + + datasets.forEach((dataset, idx) => { + const offsetX = idx * sectionWidth; + const centerX = offsetX + sectionWidth / 2; + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(dataset.title, centerX, 30); + + // Draw bars + dataset.data.forEach((val, i) => { + const barHeight = Math.abs(val) / dataset.maxVal * 200; + const x = centerX - barWidth / 2; + const y = val >= 0 ? 200 - barHeight : 200; + + ctx.fillStyle = '#6aa9ff'; + ctx.fillRect(x, y, barWidth, barHeight); + + // Value label + ctx.fillStyle = '#a9b4c2'; + ctx.font = '10px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(val.toFixed(2), centerX, val >= 0 ? y - 5 : y + barHeight + 15); + + centerX += 35; + }); + }); +} + +function drawPipeline() { + const canvas = document.getElementById('pipeline-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 300; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const steps = ['Raw Data', 'Handle Missing', 'Encode Categories', 'Scale Features', 'Train Model']; + const stepWidth = (width - 100) / steps.length; + const y = height / 2; + + steps.forEach((step, i) => { + const x = 50 + i * stepWidth; + + // Box + ctx.fillStyle = '#2a3544'; + ctx.fillRect(x, y - 30, stepWidth - 40, 60); + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 2; + ctx.strokeRect(x, y - 30, stepWidth - 40, 60); + + // Text + ctx.fillStyle = '#e8eef6'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + const words = step.split(' '); + words.forEach((word, j) => { + ctx.fillText(word, x + (stepWidth - 40) / 2, y + j * 15 - 5); + }); - if (input) { - input.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - data = parseInput(input.value); - draw(); - } - }); + // Arrow + if (i < steps.length - 1) { + ctx.strokeStyle = '#7ef0d4'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(x + stepWidth - 40, y); + ctx.lineTo(x + stepWidth - 10, y); + ctx.stroke(); + + // Arrowhead + ctx.fillStyle = '#7ef0d4'; + ctx.beginPath(); + ctx.moveTo(x + stepWidth - 10, y); + ctx.lineTo(x + stepWidth - 20, y - 5); + ctx.lineTo(x + stepWidth - 20, y + 5); + ctx.fill(); } - - draw(); + }); } -// ===== INITIALIZE ALL VISUALIZATIONS ===== -function initializeAllVisualizations() { - // Statistics visualizations - initPopulationSampleViz(); - initCentralTendencyViz(); - - // Linear Algebra visualizations - initVectorCanvas(); - initSpanCanvas(); - initTransformationGrid(); - initEigenvectorCanvas(); - - // Calculus visualizations - initCircleAreaCanvas(); - initDerivativeCanvas(); - initRiemannSumCanvas(); - initTaylorSeriesCanvas(); - - // Data Science visualizations - initSimpleRegressionCanvas(); - initLogisticRegressionCanvas(); - initPolynomialRegressionCanvas(); - initPCACanvas(); - initGradientDescentCanvas(); - initLossLandscapeCanvas(); - - // Machine Learning visualizations - initMLLinearRegressionCanvas(); - initMLKMeansCanvas(); +// Loss Functions +function initLossFunctions() { + const canvas = document.getElementById('loss-comparison-canvas'); + if (canvas && !canvas.dataset.initialized) { + canvas.dataset.initialized = 'true'; + drawLossComparison(); + } + + const canvas2 = document.getElementById('loss-curves-canvas'); + if (canvas2 && !canvas2.dataset.initialized) { + canvas2.dataset.initialized = 'true'; + drawLossCurves(); + } } -// ===== MACHINE LEARNING VISUALIZATIONS ===== +function drawLossComparison() { + const canvas = document.getElementById('loss-comparison-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const actual = [10, 20, 30, 40, 50]; + const predicted = [12, 19, 32, 38, 51]; + + // Calculate losses + let mse = 0, mae = 0; + actual.forEach((a, i) => { + const error = a - predicted[i]; + mse += error * error; + mae += Math.abs(error); + }); + mse /= actual.length; + mae /= actual.length; + const rmse = Math.sqrt(mse); + + // Display + const padding = 60; + const barHeight = 60; + const startY = 100; + const maxWidth = width - 2 * padding; + + const losses = [ + { name: 'MSE', value: mse, color: '#ff8c6a' }, + { name: 'MAE', value: mae, color: '#6aa9ff' }, + { name: 'RMSE', value: rmse, color: '#7ef0d4' } + ]; + + const maxLoss = Math.max(...losses.map(l => l.value)); + + losses.forEach((loss, i) => { + const y = startY + i * (barHeight + 30); + const barWidth = (loss.value / maxLoss) * maxWidth; + + // Bar + ctx.fillStyle = loss.color; + ctx.fillRect(padding, y, barWidth, barHeight); + + // Label + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(loss.name, padding + 10, y + barHeight / 2 + 5); + + // Value + ctx.font = '16px sans-serif'; + ctx.textAlign = 'right'; + ctx.fillText(loss.value.toFixed(2), padding + barWidth - 10, y + barHeight / 2 + 5); + }); + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Regression Loss Comparison', width / 2, 50); +} -function initMLLinearRegressionCanvas() { - const canvas = document.getElementById('canvas-ml-1'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let showLine = false; - - // House price data from worked example - const data = [ - {x: 1000, y: 150}, - {x: 1500, y: 200}, - {x: 2000, y: 250}, - {x: 3000, y: 350} - ]; - - function draw() { - clearCanvas(ctx, canvas); - - const padding = 80; - const width = canvas.width - 2 * padding; - const height = canvas.height - 2 * padding; - - const maxX = 3500; - const maxY = 400; - - // Draw axes - drawLine(ctx, padding, canvas.height - padding, canvas.width - padding, canvas.height - padding, COLORS.text, 2); - drawLine(ctx, padding, padding, padding, canvas.height - padding, COLORS.text, 2); - - // Draw grid - for (let i = 0; i <= 7; i++) { - const x = padding + (i / 7) * width; - const xVal = (i * 500).toString(); - drawLine(ctx, x, canvas.height - padding, x, canvas.height - padding + 5, COLORS.textSecondary, 1); - drawText(ctx, xVal, x, canvas.height - padding + 20, 10, COLORS.textSecondary); - } - - for (let i = 0; i <= 8; i++) { - const y = canvas.height - padding - (i / 8) * height; - const yVal = (i * 50).toString(); - drawLine(ctx, padding - 5, y, padding, y, COLORS.textSecondary, 1); - drawText(ctx, yVal, padding - 15, y + 4, 10, COLORS.textSecondary, 'right'); +function drawLossCurves() { + const canvas = document.getElementById('loss-curves-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 350; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const scaleX = (x) => padding + (x / 10) * chartWidth; + const scaleY = (y) => height - padding - (y / 100) * chartHeight; + + // Draw MSE curve + ctx.strokeStyle = '#ff8c6a'; + ctx.lineWidth = 3; + ctx.beginPath(); + for (let x = -10; x <= 10; x += 0.2) { + const y = x * x; + if (x === -10) ctx.moveTo(scaleX(x + 10), scaleY(y)); + else ctx.lineTo(scaleX(x + 10), scaleY(y)); + } + ctx.stroke(); + + // Draw MAE curve + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 3; + ctx.beginPath(); + for (let x = -10; x <= 10; x += 0.2) { + const y = Math.abs(x) * 10; + if (x === -10) ctx.moveTo(scaleX(x + 10), scaleY(y)); + else ctx.lineTo(scaleX(x + 10), scaleY(y)); + } + ctx.stroke(); + + // Legend + ctx.fillStyle = '#ff8c6a'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('MSE (quadratic penalty)', padding + 10, padding + 20); + ctx.fillStyle = '#6aa9ff'; + ctx.fillText('MAE (linear penalty)', padding + 10, padding + 40); + + // Labels + ctx.fillStyle = '#a9b4c2'; + ctx.textAlign = 'center'; + ctx.fillText('Error', width / 2, height - 20); + ctx.save(); + ctx.translate(20, height / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('Loss', 0, 0); + ctx.restore(); +} + +// Topic 13: Finding Optimal K in KNN +let elbowChart = null; +let cvKChart = null; + +function initOptimalK() { + const canvas1 = document.getElementById('elbow-canvas'); + if (canvas1 && !canvas1.dataset.initialized) { + canvas1.dataset.initialized = 'true'; + drawElbowCurve(); + } + + const canvas2 = document.getElementById('cv-k-canvas'); + if (canvas2 && !canvas2.dataset.initialized) { + canvas2.dataset.initialized = 'true'; + drawCVKHeatmap(); + } +} + +function drawElbowCurve() { + const canvas = document.getElementById('elbow-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + // Data from application_data_json + const kValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; + const accuracies = [0.96, 0.94, 0.93, 0.91, 0.89, 0.87, 0.85, 0.84, 0.83, 0.82, 0.81, 0.80, 0.79, 0.78, 0.77, 0.76, 0.75, 0.74, 0.73]; + const optimalK = 3; + + const scaleX = (k) => padding + ((k - 1) / (kValues.length - 1)) * chartWidth; + const scaleY = (acc) => height - padding - ((acc - 0.7) / 0.3) * chartHeight; + + // Draw axes + ctx.strokeStyle = '#2a3544'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(padding, padding); + ctx.lineTo(padding, height - padding); + ctx.lineTo(width - padding, height - padding); + ctx.stroke(); + + // Destroy existing chart + if (elbowChart) { + elbowChart.destroy(); + } + + // Use Chart.js + elbowChart = new Chart(ctx, { + type: 'line', + data: { + labels: kValues, + datasets: [{ + label: 'Accuracy', + data: accuracies, + borderColor: '#6aa9ff', + backgroundColor: 'rgba(106, 169, 255, 0.1)', + borderWidth: 3, + fill: true, + tension: 0.4, + pointRadius: kValues.map(k => k === optimalK ? 10 : 5), + pointBackgroundColor: kValues.map(k => k === optimalK ? '#7ef0d4' : '#6aa9ff'), + pointBorderColor: kValues.map(k => k === optimalK ? '#7ef0d4' : '#6aa9ff'), + pointBorderWidth: kValues.map(k => k === optimalK ? 3 : 2) + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { + display: true, + text: `Elbow Method: Optimal K = ${optimalK} (Accuracy: ${accuracies[optimalK - 1].toFixed(2)})`, + color: '#7ef0d4', + font: { size: 16, weight: 'bold' } + }, + legend: { + labels: { color: '#a9b4c2' } + }, + annotation: { + annotations: { + line1: { + type: 'line', + xMin: optimalK, + xMax: optimalK, + borderColor: '#7ef0d4', + borderWidth: 2, + borderDash: [5, 5] + } + } } - - // Draw labels - drawText(ctx, 'Size (sq ft)', canvas.width / 2, canvas.height - 10, 12, COLORS.cyan); - ctx.save(); - ctx.translate(20, canvas.height / 2); - ctx.rotate(-Math.PI / 2); - drawText(ctx, 'Price ($1000s)', 0, 0, 12, COLORS.cyan); - ctx.restore(); - - // Draw data points - data.forEach(point => { - const px = padding + (point.x / maxX) * width; - const py = canvas.height - padding - (point.y / maxY) * height; - drawCircle(ctx, px, py, 8, COLORS.cyan); - drawText(ctx, `${point.y}k`, px + 15, py - 10, 10, COLORS.cyan, 'left'); - }); - - // Draw regression line if enabled - if (showLine) { - // From worked example: y = 50 + 0.1x - const slope = 0.1; - const intercept = 50; - - const x1 = 0; - const y1 = intercept; - const x2 = maxX; - const y2 = intercept + slope * x2; - - const px1 = padding + (x1 / maxX) * width; - const py1 = canvas.height - padding - (y1 / maxY) * height; - const px2 = padding + (x2 / maxX) * width; - const py2 = canvas.height - padding - (y2 / maxY) * height; - - drawLine(ctx, px1, py1, px2, py2, COLORS.orange, 3); - - // Show equation - drawText(ctx, 'y = 50 + 0.10x', canvas.width / 2, 30, 16, COLORS.orange); - drawText(ctx, 'R² = 1.00 (Perfect Fit!)', canvas.width / 2, 50, 14, COLORS.green); - - // Highlight prediction point (2500, 300) - const predX = 2500; - const predY = 50 + 0.1 * predX; - const ppx = padding + (predX / maxX) * width; - const ppy = canvas.height - padding - (predY / maxY) * height; - drawCircle(ctx, ppx, ppy, 10, COLORS.green); - drawText(ctx, '2500 sq ft → $300k', ppx - 80, ppy - 15, 12, COLORS.green, 'left'); + }, + scales: { + x: { + title: { display: true, text: 'K (Number of Neighbors)', color: '#a9b4c2' }, + grid: { color: '#2a3544' }, + ticks: { color: '#a9b4c2' } + }, + y: { + title: { display: true, text: 'Accuracy', color: '#a9b4c2' }, + grid: { color: '#2a3544' }, + ticks: { color: '#a9b4c2' }, + min: 0.7, + max: 1.0 } + } } - - const fitBtn = document.getElementById('btn-ml-1-fit'); - const resetBtn = document.getElementById('btn-ml-1-reset'); - - if (fitBtn) { - fitBtn.addEventListener('click', () => { - showLine = true; - draw(); - }); - } - - if (resetBtn) { - resetBtn.addEventListener('click', () => { - showLine = false; - draw(); - }); - } - - draw(); + }, 'Elbow Chart'); } -function initMLKMeansCanvas() { - const canvas = document.getElementById('canvas-ml-15'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let clustered = false; - - // Customer data from worked example - const customers = [ - {name: 'A', age: 25, income: 40, cluster: null}, - {name: 'B', age: 30, income: 50, cluster: null}, - {name: 'C', age: 28, income: 45, cluster: null}, - {name: 'D', age: 55, income: 80, cluster: null}, - {name: 'E', age: 60, income: 90, cluster: null}, - {name: 'F', age: 52, income: 75, cluster: null} - ]; - - let centroids = [ - {age: 25, income: 40, color: COLORS.cyan}, - {age: 60, income: 90, color: COLORS.orange} - ]; - - function assignClusters() { - customers.forEach(customer => { - // Calculate distance to each centroid - const d1 = Math.sqrt(Math.pow(customer.age - centroids[0].age, 2) + Math.pow(customer.income - centroids[0].income, 2)); - const d2 = Math.sqrt(Math.pow(customer.age - centroids[1].age, 2) + Math.pow(customer.income - centroids[1].income, 2)); - - customer.cluster = d1 < d2 ? 0 : 1; - }); - - // Update centroids - const cluster0 = customers.filter(c => c.cluster === 0); - const cluster1 = customers.filter(c => c.cluster === 1); - - if (cluster0.length > 0) { - centroids[0].age = cluster0.reduce((s, c) => s + c.age, 0) / cluster0.length; - centroids[0].income = cluster0.reduce((s, c) => s + c.income, 0) / cluster0.length; - } - if (cluster1.length > 0) { - centroids[1].age = cluster1.reduce((s, c) => s + c.age, 0) / cluster1.length; - centroids[1].income = cluster1.reduce((s, c) => s + c.income, 0) / cluster1.length; - } - } +function drawCVKHeatmap() { + const canvas = document.getElementById('cv-k-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 80; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const kValues = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]; + const folds = ['Fold 1', 'Fold 2', 'Fold 3']; + const fold1 = [0.98, 0.92, 0.88, 0.85, 0.83, 0.81, 0.79, 0.77, 0.75, 0.73]; + const fold2 = [0.96, 0.91, 0.87, 0.83, 0.81, 0.79, 0.77, 0.75, 0.73, 0.71]; + const fold3 = [0.94, 0.90, 0.86, 0.82, 0.79, 0.77, 0.75, 0.73, 0.71, 0.69]; + const allData = [fold1, fold2, fold3]; + + const cellWidth = chartWidth / kValues.length; + const cellHeight = chartHeight / folds.length; + + // Draw heatmap + folds.forEach((fold, i) => { + kValues.forEach((k, j) => { + const acc = allData[i][j]; + const x = padding + j * cellWidth; + const y = padding + i * cellHeight; + + // Color based on accuracy + const intensity = (acc - 0.65) / 0.35; + const r = Math.floor(106 + (126 - 106) * intensity); + const g = Math.floor(169 + (240 - 169) * intensity); + const b = Math.floor(255 + (212 - 255) * intensity); + ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; + ctx.fillRect(x, y, cellWidth, cellHeight); + + // Border + ctx.strokeStyle = '#1a2332'; + ctx.lineWidth = 1; + ctx.strokeRect(x, y, cellWidth, cellHeight); + + // Text + ctx.fillStyle = '#1a2332'; + ctx.font = 'bold 11px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(acc.toFixed(2), x + cellWidth / 2, y + cellHeight / 2 + 4); + }); + }); + + // Row labels + ctx.fillStyle = '#e8eef6'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'right'; + folds.forEach((fold, i) => { + const y = padding + i * cellHeight + cellHeight / 2; + ctx.fillText(fold, padding - 10, y + 4); + }); + + // Column labels + ctx.textAlign = 'center'; + kValues.forEach((k, j) => { + const x = padding + j * cellWidth + cellWidth / 2; + ctx.fillText(`K=${k}`, x, padding - 10); + }); + + // Mean accuracy + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + const meanAccs = kValues.map((k, j) => { + const sum = fold1[j] + fold2[j] + fold3[j]; + return sum / 3; + }); + const maxMean = Math.max(...meanAccs); + const optIdx = meanAccs.indexOf(maxMean); + ctx.fillText(`Best K = ${kValues[optIdx]} (Mean Acc: ${maxMean.toFixed(3)})`, padding, height - 20); +} + +// Topic 14: Hyperparameter Tuning +let gridSearchChart = null; + +function initHyperparameterTuning() { + const canvas1 = document.getElementById('gridsearch-heatmap'); + if (canvas1 && !canvas1.dataset.initialized) { + canvas1.dataset.initialized = 'true'; + drawGridSearchHeatmap(); + } + + const canvas2 = document.getElementById('param-surface'); + if (canvas2 && !canvas2.dataset.initialized) { + canvas2.dataset.initialized = 'true'; + drawParamSurface(); + } + + const radios = document.querySelectorAll('input[name="grid-model"]'); + radios.forEach(radio => { + radio.addEventListener('change', () => { + drawGridSearchHeatmap(); + }); + }); +} + +function drawGridSearchHeatmap() { + const canvas = document.getElementById('gridsearch-heatmap'); + if (!canvas) return; + + // Destroy existing chart + if (gridSearchChart) { + gridSearchChart.destroy(); + } + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 450; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 80; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const cValues = [0.1, 1, 10, 100]; + const gammaValues = [0.001, 0.01, 0.1, 1]; + + // Simulate accuracy grid + const accuracies = [ + [0.65, 0.82, 0.88, 0.75], + [0.78, 0.91, 0.95, 0.89], + [0.85, 0.93, 0.92, 0.87], + [0.80, 0.88, 0.84, 0.82] + ]; + + const cellWidth = chartWidth / cValues.length; + const cellHeight = chartHeight / gammaValues.length; + + let bestAcc = 0, bestI = 0, bestJ = 0; + + // Draw heatmap + gammaValues.forEach((gamma, i) => { + cValues.forEach((c, j) => { + const acc = accuracies[i][j]; + if (acc > bestAcc) { + bestAcc = acc; + bestI = i; + bestJ = j; + } + + const x = padding + j * cellWidth; + const y = padding + i * cellHeight; + + // Color gradient + const intensity = (acc - 0.6) / 0.35; + const r = Math.floor(255 - 149 * intensity); + const g = Math.floor(140 + 100 * intensity); + const b = Math.floor(106 + 106 * intensity); + ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; + ctx.fillRect(x, y, cellWidth, cellHeight); + + // Border + ctx.strokeStyle = '#1a2332'; + ctx.lineWidth = 2; + ctx.strokeRect(x, y, cellWidth, cellHeight); + + // Text + ctx.fillStyle = '#1a2332'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(acc.toFixed(2), x + cellWidth / 2, y + cellHeight / 2 + 5); + }); + }); + + // Highlight best + const bestX = padding + bestJ * cellWidth; + const bestY = padding + bestI * cellHeight; + ctx.strokeStyle = '#7ef0d4'; + ctx.lineWidth = 4; + ctx.strokeRect(bestX, bestY, cellWidth, cellHeight); + + // Labels + ctx.fillStyle = '#e8eef6'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'right'; + gammaValues.forEach((gamma, i) => { + const y = padding + i * cellHeight + cellHeight / 2; + ctx.fillText(`γ=${gamma}`, padding - 10, y + 5); + }); + + ctx.textAlign = 'center'; + cValues.forEach((c, j) => { + const x = padding + j * cellWidth + cellWidth / 2; + ctx.fillText(`C=${c}`, x, padding - 10); + }); + + // Axis labels + ctx.fillStyle = '#a9b4c2'; + ctx.font = 'bold 14px sans-serif'; + ctx.fillText('C Parameter', width / 2, height - 30); + ctx.save(); + ctx.translate(25, height / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('Gamma Parameter', 0, 0); + ctx.restore(); + + // Best params - Use Chart.js for bar comparison instead + const compareData = []; + cValues.forEach((c, j) => { + gammaValues.forEach((g, i) => { + compareData.push({ + c: c, + gamma: g, + acc: accuracies[i][j], + label: `C=${c}, γ=${g}` + }); + }); + }); + + // Sort and get top 5 + compareData.sort((a, b) => b.acc - a.acc); + const top5 = compareData.slice(0, 5); + + // Add annotation for best + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(`Best: C=${cValues[bestJ]}, γ=${gammaValues[bestI]} → Acc=${bestAcc.toFixed(2)}`, padding, height - 30); +} + +function drawParamSurface() { + const canvas = document.getElementById('param-surface'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const centerX = width / 2; + const centerY = height / 2; + + // Draw 3D-ish surface using contour lines + const levels = [0.65, 0.70, 0.75, 0.80, 0.85, 0.90, 0.95]; + const colors = ['#ff8c6a', '#ffa07a', '#ffb490', '#ffc8a6', '#7ef0d4', '#6aa9ff', '#5a99ef']; + + levels.forEach((level, i) => { + const radius = 150 - i * 20; + ctx.strokeStyle = colors[i]; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.ellipse(centerX, centerY, radius, radius * 0.6, 0, 0, 2 * Math.PI); + ctx.stroke(); - function draw() { - clearCanvas(ctx, canvas); - - const padding = 80; - const width = canvas.width - 2 * padding; - const height = canvas.height - 2 * padding; - - const minAge = 20, maxAge = 70; - const minIncome = 30, maxIncome = 100; - - // Draw axes - drawLine(ctx, padding, canvas.height - padding, canvas.width - padding, canvas.height - padding, COLORS.text, 2); - drawLine(ctx, padding, padding, padding, canvas.height - padding, COLORS.text, 2); - - // Draw grid - for (let age = 20; age <= 70; age += 10) { - const x = padding + ((age - minAge) / (maxAge - minAge)) * width; - drawLine(ctx, x, canvas.height - padding, x, canvas.height - padding + 5, COLORS.textSecondary, 1); - drawText(ctx, age.toString(), x, canvas.height - padding + 20, 10, COLORS.textSecondary); + // Label + ctx.fillStyle = colors[i]; + ctx.font = '11px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(level.toFixed(2), centerX + radius + 10, centerY); + }); + + // Center point (optimum) + ctx.fillStyle = '#7ef0d4'; + ctx.beginPath(); + ctx.arc(centerX, centerY, 8, 0, 2 * Math.PI); + ctx.fill(); + + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Optimal Point', centerX, centerY - 20); + ctx.fillText('(C=1, γ=scale)', centerX, centerY + 35); + + // Axis labels + ctx.fillStyle = '#a9b4c2'; + ctx.font = '12px sans-serif'; + ctx.fillText('C Parameter →', width - 80, height - 20); + ctx.save(); + ctx.translate(30, 60); + ctx.rotate(-Math.PI / 2); + ctx.fillText('← Gamma', 0, 0); + ctx.restore(); + + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Performance Surface (3D Contour View)', width / 2, 30); +} + +// Topic 15: Naive Bayes +let bayesComparisonChart = null; +let categoricalNBChart = null; +let gaussianNBChart = null; + +function initNaiveBayes() { + const canvas1 = document.getElementById('bayes-theorem-viz'); + if (canvas1 && !canvas1.dataset.initialized) { + canvas1.dataset.initialized = 'true'; + drawBayesTheorem(); + } + + const canvas2 = document.getElementById('spam-classification'); + if (canvas2 && !canvas2.dataset.initialized) { + canvas2.dataset.initialized = 'true'; + drawSpamClassification(); + } + + const canvas3 = document.getElementById('categorical-nb-canvas'); + if (canvas3 && !canvas3.dataset.initialized) { + canvas3.dataset.initialized = 'true'; + drawCategoricalNB(); + } + + const canvas4 = document.getElementById('gaussian-nb-canvas'); + if (canvas4 && !canvas4.dataset.initialized) { + canvas4.dataset.initialized = 'true'; + drawGaussianNB(); + } +} + +function drawBayesTheorem() { + const canvas = document.getElementById('bayes-theorem-viz'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const centerX = width / 2; + const centerY = height / 2; + + // Draw formula components as boxes + const boxes = [ + { x: centerX - 300, y: centerY - 80, w: 120, h: 60, text: 'P(C|F)', label: 'Posterior', color: '#7ef0d4' }, + { x: centerX - 100, y: centerY - 80, w: 120, h: 60, text: 'P(F|C)', label: 'Likelihood', color: '#6aa9ff' }, + { x: centerX + 100, y: centerY - 80, w: 100, h: 60, text: 'P(C)', label: 'Prior', color: '#ffb490' }, + { x: centerX - 50, y: centerY + 60, w: 100, h: 60, text: 'P(F)', label: 'Evidence', color: '#ff8c6a' } + ]; + + boxes.forEach(box => { + ctx.fillStyle = box.color + '33'; + ctx.fillRect(box.x, box.y, box.w, box.h); + ctx.strokeStyle = box.color; + ctx.lineWidth = 2; + ctx.strokeRect(box.x, box.y, box.w, box.h); + + ctx.fillStyle = box.color; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(box.text, box.x + box.w / 2, box.y + box.h / 2); + + ctx.font = '12px sans-serif'; + ctx.fillStyle = '#a9b4c2'; + ctx.fillText(box.label, box.x + box.w / 2, box.y + box.h + 20); + }); + + // Draw arrows and operators + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 20px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('=', centerX - 160, centerY - 40); + ctx.fillText('×', centerX + 40, centerY - 40); + ctx.fillText('÷', centerX, centerY + 20); + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 18px sans-serif'; + ctx.fillText("Bayes' Theorem Breakdown", centerX, 40); +} + +function drawCategoricalNB() { + const canvas = document.getElementById('categorical-nb-canvas'); + if (!canvas) return; + + if (categoricalNBChart) { + categoricalNBChart.destroy(); + } + + const ctx = canvas.getContext('2d'); + + categoricalNBChart = safeCreateChart(ctx, { + type: 'bar', + data: { + labels: ['P(Yes|Rainy,Hot)', 'P(No|Rainy,Hot)'], + datasets: [{ + label: 'Without Smoothing', + data: [0.0833, 0], + backgroundColor: 'rgba(255, 140, 106, 0.6)', + borderColor: '#ff8c6a', + borderWidth: 2 + }, { + label: 'With Laplace Smoothing', + data: [0.0818, 0.0266], + backgroundColor: 'rgba(126, 240, 212, 0.6)', + borderColor: '#7ef0d4', + borderWidth: 2 + }, { + label: 'Normalized Probability', + data: [0.755, 0.245], + backgroundColor: 'rgba(106, 169, 255, 0.8)', + borderColor: '#6aa9ff', + borderWidth: 2 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { + display: true, + text: 'Categorical Naive Bayes: Probability Comparison', + color: '#e8eef6', + font: { size: 16, weight: 'bold' } + }, + legend: { + labels: { color: '#a9b4c2' } } - - for (let income = 30; income <= 100; income += 10) { - const y = canvas.height - padding - ((income - minIncome) / (maxIncome - minIncome)) * height; - drawLine(ctx, padding - 5, y, padding, y, COLORS.textSecondary, 1); - drawText(ctx, `$${income}k`, padding - 40, y + 4, 10, COLORS.textSecondary, 'right'); + }, + scales: { + x: { + grid: { color: '#2a3544' }, + ticks: { color: '#a9b4c2' } + }, + y: { + title: { display: true, text: 'Probability', color: '#a9b4c2' }, + grid: { color: '#2a3544' }, + ticks: { color: '#a9b4c2' }, + min: 0, + max: 1 } - - // Draw labels - drawText(ctx, 'Age', canvas.width / 2, canvas.height - 10, 12, COLORS.cyan); - ctx.save(); - ctx.translate(20, canvas.height / 2); - ctx.rotate(-Math.PI / 2); - drawText(ctx, 'Income ($k)', 0, 0, 12, COLORS.cyan); - ctx.restore(); - - // Draw customers - customers.forEach(customer => { - const px = padding + ((customer.age - minAge) / (maxAge - minAge)) * width; - const py = canvas.height - padding - ((customer.income - minIncome) / (maxIncome - minIncome)) * height; - - const color = clustered ? (customer.cluster === 0 ? COLORS.cyan : COLORS.orange) : COLORS.primary; - drawCircle(ctx, px, py, 10, color); - drawText(ctx, customer.name, px, py - 15, 12, COLORS.text); - }); - - // Draw centroids if clustered - if (clustered) { - centroids.forEach((centroid, i) => { - const cx = padding + ((centroid.age - minAge) / (maxAge - minAge)) * width; - const cy = canvas.height - padding - ((centroid.income - minIncome) / (maxIncome - minIncome)) * height; - - // Draw X marker for centroid - ctx.strokeStyle = centroid.color; - ctx.lineWidth = 4; - ctx.beginPath(); - ctx.moveTo(cx - 12, cy - 12); - ctx.lineTo(cx + 12, cy + 12); - ctx.moveTo(cx + 12, cy - 12); - ctx.lineTo(cx - 12, cy + 12); - ctx.stroke(); - - drawText(ctx, `C${i+1} [${centroid.age.toFixed(1)}, ${centroid.income.toFixed(1)}]`, - cx + 20, cy, 11, centroid.color, 'left'); - }); - - drawText(ctx, 'Cluster 1 (Young, Lower Income)', 150, 30, 12, COLORS.cyan); - drawText(ctx, 'Cluster 2 (Mature, Higher Income)', 150, 50, 12, COLORS.orange); - } - } - - const clusterBtn = document.getElementById('btn-ml-15-cluster'); - const resetBtn = document.getElementById('btn-ml-15-reset'); - - if (clusterBtn) { - clusterBtn.addEventListener('click', () => { - clustered = true; - assignClusters(); - draw(); - }); - } - - if (resetBtn) { - resetBtn.addEventListener('click', () => { - clustered = false; - customers.forEach(c => c.cluster = null); - centroids = [ - {age: 25, income: 40, color: COLORS.cyan}, - {age: 60, income: 90, color: COLORS.orange} - ]; - draw(); - }); + } } - - draw(); + }, 'Categorical Naive Bayes Chart'); } -// ===== LINEAR ALGEBRA VISUALIZATIONS ===== +function drawGaussianNB() { + const canvas = document.getElementById('gaussian-nb-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const xMin = 0, xMax = 5, yMin = 0, yMax = 4; + const scaleX = (x) => padding + (x / xMax) * chartWidth; + const scaleY = (y) => height - padding - (y / yMax) * chartHeight; + + // Draw decision boundary (approximate) + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 3; + ctx.setLineDash([5, 5]); + ctx.beginPath(); + ctx.moveTo(scaleX(2.5), scaleY(0)); + ctx.lineTo(scaleX(2.5), scaleY(4)); + ctx.stroke(); + ctx.setLineDash([]); + + // Draw "Yes" points + const yesPoints = [{x: 1.0, y: 2.0}, {x: 2.0, y: 1.0}, {x: 1.5, y: 1.8}]; + yesPoints.forEach(p => { + ctx.fillStyle = '#7ef0d4'; + ctx.beginPath(); + ctx.arc(scaleX(p.x), scaleY(p.y), 8, 0, 2 * Math.PI); + ctx.fill(); + ctx.strokeStyle = '#1a2332'; + ctx.lineWidth = 2; + ctx.stroke(); + }); + + // Draw "No" points + const noPoints = [{x: 3.0, y: 3.0}, {x: 3.5, y: 2.8}, {x: 2.9, y: 3.2}]; + noPoints.forEach(p => { + ctx.fillStyle = '#ff8c6a'; + ctx.beginPath(); + ctx.arc(scaleX(p.x), scaleY(p.y), 8, 0, 2 * Math.PI); + ctx.fill(); + ctx.strokeStyle = '#1a2332'; + ctx.lineWidth = 2; + ctx.stroke(); + }); + + // Draw test point + ctx.fillStyle = '#ffeb3b'; + ctx.beginPath(); + ctx.arc(scaleX(2.0), scaleY(2.0), 12, 0, 2 * Math.PI); + ctx.fill(); + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 3; + ctx.stroke(); + + // Label test point + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Test [2.0, 2.0]', scaleX(2.0), scaleY(2.0) - 20); + ctx.fillStyle = '#7ef0d4'; + ctx.fillText('→ YES', scaleX(2.0), scaleY(2.0) + 30); + + // Axes + ctx.strokeStyle = '#2a3544'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(padding, padding); + ctx.lineTo(padding, height - padding); + ctx.lineTo(width - padding, height - padding); + ctx.stroke(); + + // Labels + ctx.fillStyle = '#a9b4c2'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('X₁', width / 2, height - 20); + ctx.save(); + ctx.translate(20, height / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('X₂', 0, 0); + ctx.restore(); + + // Legend + ctx.fillStyle = '#7ef0d4'; + ctx.beginPath(); + ctx.arc(padding + 20, 30, 6, 0, 2 * Math.PI); + ctx.fill(); + ctx.fillStyle = '#e8eef6'; + ctx.font = '11px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Class: Yes', padding + 30, 35); + + ctx.fillStyle = '#ff8c6a'; + ctx.beginPath(); + ctx.arc(padding + 120, 30, 6, 0, 2 * Math.PI); + ctx.fill(); + ctx.fillStyle = '#e8eef6'; + ctx.fillText('Class: No', padding + 130, 35); + + ctx.fillStyle = '#6aa9ff'; + ctx.fillText('| Decision Boundary', padding + 210, 35); +} -function initVectorCanvas() { - const canvas = document.getElementById('canvas-42'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let vx = 3, vy = 2; - - function draw() { - clearCanvas(ctx, canvas); - - const centerX = canvas.width / 2; - const centerY = canvas.height / 2; - const scale = 40; - - // Draw axes - drawLine(ctx, 0, centerY, canvas.width, centerY, '#555', 1); - drawLine(ctx, centerX, 0, centerX, canvas.height, '#555', 1); - - // Draw grid - ctx.strokeStyle = '#333'; - ctx.lineWidth = 0.5; - for (let i = -10; i <= 10; i++) { - if (i !== 0) { - drawLine(ctx, centerX + i * scale, 0, centerX + i * scale, canvas.height, '#333', 0.5); - drawLine(ctx, 0, centerY + i * scale, canvas.width, centerY + i * scale, '#333', 0.5); - } +function drawSpamClassification() { + const canvas = document.getElementById('spam-classification'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 40; + const stepHeight = 70; + const startY = 60; + + // Step 1: Features + ctx.fillStyle = '#6aa9ff'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Step 1: Email Features', padding, startY); + ctx.fillStyle = '#e8eef6'; + ctx.font = '13px sans-serif'; + ctx.fillText('Words: ["free", "winner", "click"]', padding + 20, startY + 25); + + // Step 2: Calculate P(spam) + const y2 = startY + stepHeight; + ctx.fillStyle = '#6aa9ff'; + ctx.font = 'bold 14px sans-serif'; + ctx.fillText('Step 2: P(spam | features)', padding, y2); + ctx.fillStyle = '#e8eef6'; + ctx.font = '12px monospace'; + ctx.fillText('= P("free"|spam) × P("winner"|spam) × P("click"|spam) × P(spam)', padding + 20, y2 + 25); + ctx.fillText('= 0.8 × 0.7 × 0.6 × 0.3', padding + 20, y2 + 45); + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 14px monospace'; + ctx.fillText('= 0.1008', padding + 20, y2 + 65); + + // Step 3: Calculate P(not spam) + const y3 = y2 + stepHeight + 50; + ctx.fillStyle = '#6aa9ff'; + ctx.font = 'bold 14px sans-serif'; + ctx.fillText('Step 3: P(not-spam | features)', padding, y3); + ctx.fillStyle = '#e8eef6'; + ctx.font = '12px monospace'; + ctx.fillText('= P("free"|not-spam) × P("winner"|not-spam) × P("click"|not-spam) × P(not-spam)', padding + 20, y3 + 25); + ctx.fillText('= 0.1 × 0.05 × 0.2 × 0.7', padding + 20, y3 + 45); + ctx.fillStyle = '#ff8c6a'; + ctx.font = 'bold 14px monospace'; + ctx.fillText('= 0.0007', padding + 20, y3 + 65); + + // Step 4: Decision + const y4 = y3 + stepHeight + 50; + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.fillText('Decision: 0.1008 > 0.0007', padding, y4); + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 18px sans-serif'; + ctx.fillText('→ SPAM! 📧❌', padding, y4 + 30); + + // Create comparison chart at bottom + if (!bayesComparisonChart) { + const compCanvas = document.createElement('canvas'); + compCanvas.id = 'bayes-comparison-chart'; + compCanvas.style.marginTop = '20px'; + canvas.parentElement.appendChild(compCanvas); + + bayesComparisonChart = safeCreateChart(compCanvas.getContext('2d'), { + type: 'bar', + data: { + labels: ['Spam Probability', 'Not-Spam Probability'], + datasets: [{ + label: 'Probability', + data: [0.1008, 0.0007], + backgroundColor: ['#7ef0d4', '#ff8c6a'], + borderColor: ['#7ef0d4', '#ff8c6a'], + borderWidth: 2 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + indexAxis: 'y', + plugins: { + title: { + display: true, + text: 'Probability Comparison', + color: '#e8eef6', + font: { size: 14 } + }, + legend: { display: false } + }, + scales: { + x: { + grid: { color: '#2a3544' }, + ticks: { color: '#a9b4c2' } + }, + y: { + grid: { display: false }, + ticks: { color: '#a9b4c2' } + } } - - // Draw vector - const endX = centerX + vx * scale; - const endY = centerY - vy * scale; - - // Arrow shaft - drawLine(ctx, centerX, centerY, endX, endY, COLORS.cyan, 3); - - // Arrow head - const angle = Math.atan2(vy, vx); - const arrowSize = 15; - ctx.beginPath(); - ctx.moveTo(endX, endY); - ctx.lineTo(endX - arrowSize * Math.cos(angle - Math.PI / 6), endY + arrowSize * Math.sin(angle - Math.PI / 6)); - ctx.lineTo(endX - arrowSize * Math.cos(angle + Math.PI / 6), endY + arrowSize * Math.sin(angle + Math.PI / 6)); - ctx.closePath(); - ctx.fillStyle = COLORS.cyan; - ctx.fill(); - - // Draw vector label - drawText(ctx, `v = [${vx.toFixed(1)}, ${vy.toFixed(1)}]`, endX + 20, endY - 10, 14, COLORS.cyan, 'left'); - - // Draw magnitude - const magnitude = Math.sqrt(vx * vx + vy * vy); - drawText(ctx, `|v| = ${magnitude.toFixed(2)}`, canvas.width / 2, 30, 14, COLORS.text); - } - - const sliderX = document.getElementById('slider42x'); - const sliderY = document.getElementById('slider42y'); - const labelX = document.getElementById('vec42x'); - const labelY = document.getElementById('vec42y'); - - if (sliderX) { - sliderX.addEventListener('input', (e) => { - vx = parseFloat(e.target.value); - if (labelX) labelX.textContent = vx.toFixed(1); - draw(); - }); - } - - if (sliderY) { - sliderY.addEventListener('input', (e) => { - vy = parseFloat(e.target.value); - if (labelY) labelY.textContent = vy.toFixed(1); - draw(); - }); - } - - const resetBtn = document.getElementById('btn42reset'); - if (resetBtn) { - resetBtn.addEventListener('click', () => { - vx = 3; vy = 2; - if (sliderX) sliderX.value = vx; - if (sliderY) sliderY.value = vy; - if (labelX) labelX.textContent = vx; - if (labelY) labelY.textContent = vy; - draw(); - }); - } - - draw(); + } + }, 'Bayes Comparison Chart'); + if (bayesComparisonChart) compCanvas.style.height = '150px'; + } } -function initSpanCanvas() { - const canvas = document.getElementById('canvas-43'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let animating = false; - let t = 0; - - function draw() { - clearCanvas(ctx, canvas); - - const centerX = canvas.width / 2; - const centerY = canvas.height / 2; - const scale = 50; - - // Draw axes - drawLine(ctx, 0, centerY, canvas.width, centerY, '#555', 1); - drawLine(ctx, centerX, 0, centerX, canvas.height, '#555', 1); - - // Basis vectors - const v1 = { x: 2, y: 1 }; - const v2 = { x: -1, y: 1.5 }; - - // Draw basis vectors - drawLine(ctx, centerX, centerY, centerX + v1.x * scale, centerY - v1.y * scale, COLORS.cyan, 3); - drawLine(ctx, centerX, centerY, centerX + v2.x * scale, centerY - v2.y * scale, COLORS.orange, 3); - - if (animating) { - // Draw span (multiple linear combinations) - ctx.globalAlpha = 0.3; - for (let a = -2; a <= 2; a += 0.3) { - for (let b = -2; b <= 2; b += 0.3) { - const x = centerX + (a * v1.x + b * v2.x) * scale; - const y = centerY - (a * v1.y + b * v2.y) * scale; - drawCircle(ctx, x, y, 2, COLORS.primary); - } - } - ctx.globalAlpha = 1; - } - - drawText(ctx, 'v₁', centerX + v1.x * scale + 20, centerY - v1.y * scale, 16, COLORS.cyan); - drawText(ctx, 'v₂', centerX + v2.x * scale - 20, centerY - v2.y * scale, 16, COLORS.orange); - } - - const animateBtn = document.getElementById('btn43animate'); - const resetBtn = document.getElementById('btn43reset'); - - if (animateBtn) { - animateBtn.addEventListener('click', () => { - animating = true; - draw(); - }); - } - - if (resetBtn) { - resetBtn.addEventListener('click', () => { - animating = false; - draw(); - }); - } - - draw(); +// Topic 16: Decision Trees +function initDecisionTrees() { + const canvas1 = document.getElementById('decision-tree-viz'); + if (canvas1 && !canvas1.dataset.initialized) { + canvas1.dataset.initialized = 'true'; + drawDecisionTree(); + } + + const canvas2 = document.getElementById('entropy-viz'); + if (canvas2 && !canvas2.dataset.initialized) { + canvas2.dataset.initialized = 'true'; + drawEntropyViz(); + } + + const canvas3 = document.getElementById('split-comparison'); + if (canvas3 && !canvas3.dataset.initialized) { + canvas3.dataset.initialized = 'true'; + drawSplitComparison(); + } + + const canvas4 = document.getElementById('tree-boundary'); + if (canvas4 && !canvas4.dataset.initialized) { + canvas4.dataset.initialized = 'true'; + drawTreeBoundary(); + } } -function initTransformationGrid() { - const canvas = document.getElementById('canvas-44'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let matrix = [[1, 0], [0, 1]]; // Identity +function drawDecisionTree() { + const canvas = document.getElementById('decision-tree-viz'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 450; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const centerX = width / 2; + + // Node structure + const nodes = [ + { x: centerX, y: 60, text: 'Has "free"?', type: 'root' }, + { x: centerX - 150, y: 160, text: 'Has link?', type: 'internal' }, + { x: centerX + 150, y: 160, text: 'Sender new?', type: 'internal' }, + { x: centerX - 220, y: 260, text: 'SPAM', type: 'leaf', class: 'spam' }, + { x: centerX - 80, y: 260, text: 'NOT SPAM', type: 'leaf', class: 'not-spam' }, + { x: centerX + 80, y: 260, text: 'SPAM', type: 'leaf', class: 'spam' }, + { x: centerX + 220, y: 260, text: 'NOT SPAM', type: 'leaf', class: 'not-spam' } + ]; + + const edges = [ + { from: 0, to: 1, label: 'Yes' }, + { from: 0, to: 2, label: 'No' }, + { from: 1, to: 3, label: 'Yes' }, + { from: 1, to: 4, label: 'No' }, + { from: 2, to: 5, label: 'Yes' }, + { from: 2, to: 6, label: 'No' } + ]; + + // Draw edges + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 2; + edges.forEach(edge => { + const from = nodes[edge.from]; + const to = nodes[edge.to]; + ctx.beginPath(); + ctx.moveTo(from.x, from.y + 25); + ctx.lineTo(to.x, to.y - 25); + ctx.stroke(); - function drawGrid(transform = false) { - clearCanvas(ctx, canvas); - - const centerX = canvas.width / 2; - const centerY = canvas.height / 2; - const scale = 40; - const gridSize = 5; - - ctx.strokeStyle = transform ? COLORS.cyan : '#555'; - ctx.lineWidth = 1; - - // Draw grid lines - for (let i = -gridSize; i <= gridSize; i++) { - for (let j = -gridSize; j <= gridSize; j++) { - let x1 = i, y1 = j; - let x2 = i + 1, y2 = j; - let x3 = i, y3 = j + 1; - - if (transform) { - [x1, y1] = [matrix[0][0] * i + matrix[0][1] * j, matrix[1][0] * i + matrix[1][1] * j]; - [x2, y2] = [matrix[0][0] * (i + 1) + matrix[0][1] * j, matrix[1][0] * (i + 1) + matrix[1][1] * j]; - [x3, y3] = [matrix[0][0] * i + matrix[0][1] * (j + 1), matrix[1][0] * i + matrix[1][1] * (j + 1)]; - } - - drawLine(ctx, centerX + x1 * scale, centerY - y1 * scale, centerX + x2 * scale, centerY - y2 * scale, ctx.strokeStyle, 1); - drawLine(ctx, centerX + x1 * scale, centerY - y1 * scale, centerX + x3 * scale, centerY - y3 * scale, ctx.strokeStyle, 1); - } - } - - // Draw i-hat and j-hat - const iHat = transform ? [matrix[0][0], matrix[1][0]] : [1, 0]; - const jHat = transform ? [matrix[0][1], matrix[1][1]] : [0, 1]; - - drawLine(ctx, centerX, centerY, centerX + iHat[0] * scale, centerY - iHat[1] * scale, COLORS.orange, 3); - drawLine(ctx, centerX, centerY, centerX + jHat[0] * scale, centerY - jHat[1] * scale, COLORS.green, 3); + // Edge label + ctx.fillStyle = '#7ef0d4'; + ctx.font = '11px sans-serif'; + ctx.textAlign = 'center'; + const midX = (from.x + to.x) / 2; + const midY = (from.y + to.y) / 2; + ctx.fillText(edge.label, midX + 15, midY); + }); + + // Draw nodes + nodes.forEach(node => { + if (node.type === 'leaf') { + ctx.fillStyle = node.class === 'spam' ? '#ff8c6a33' : '#7ef0d433'; + ctx.strokeStyle = node.class === 'spam' ? '#ff8c6a' : '#7ef0d4'; + } else { + ctx.fillStyle = '#6aa9ff33'; + ctx.strokeStyle = '#6aa9ff'; } - const select = document.getElementById('select44'); - const applyBtn = document.getElementById('btn44apply'); - - if (applyBtn && select) { - applyBtn.addEventListener('click', () => { - const type = select.value; - switch (type) { - case 'rotation': - matrix = [[0, -1], [1, 0]]; - break; - case 'shear': - matrix = [[1, 1], [0, 1]]; - break; - case 'reflection': - matrix = [[1, 0], [0, -1]]; - break; - default: - matrix = [[1, 0], [0, 1]]; - } - drawGrid(true); - }); - } + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.rect(node.x - 60, node.y - 20, 120, 40); + ctx.fill(); + ctx.stroke(); - drawGrid(false); + ctx.fillStyle = '#e8eef6'; + ctx.font = node.type === 'leaf' ? 'bold 13px sans-serif' : '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(node.text, node.x, node.y + 5); + }); + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.fillText('Decision Tree: Email Spam Classifier', centerX, 30); + + // Example path + ctx.fillStyle = '#a9b4c2'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Example: Email with "free" + link → SPAM', 40, height - 20); } -function initEigenvectorCanvas() { - const canvas = document.getElementById('canvas-54'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let transformed = false; - - function draw() { - clearCanvas(ctx, canvas); - - const centerX = canvas.width / 2; - const centerY = canvas.height / 2; - const scale = 50; - - // Draw axes - drawLine(ctx, 0, centerY, canvas.width, centerY, '#555', 1); - drawLine(ctx, centerX, 0, centerX, canvas.height, '#555', 1); - - // Matrix [[2, 0], [0, 1]] - scaling transformation - // Eigenvectors: [1, 0] with eigenvalue 2, [0, 1] with eigenvalue 1 - - const e1Scale = transformed ? 2 : 1; - const e2Scale = 1; - - // Draw regular vectors (affected) - const regularVecs = [[2, 2], [1, 2], [-2, 1]]; - regularVecs.forEach(([x, y]) => { - const endX = transformed ? centerX + 2 * x * scale : centerX + x * scale; - const endY = centerY - y * scale; - drawLine(ctx, centerX, centerY, endX, endY, '#666', 2); - }); - - // Draw eigenvectors (special - stay on their line) - drawLine(ctx, centerX, centerY, centerX + e1Scale * 2 * scale, centerY, COLORS.cyan, 4); - drawLine(ctx, centerX, centerY, centerX, centerY - 2 * scale, COLORS.orange, 4); - - drawText(ctx, 'Eigenvector 1 (λ=2)', centerX + e1Scale * 2 * scale + 10, centerY - 10, 12, COLORS.cyan, 'left'); - drawText(ctx, 'Eigenvector 2 (λ=1)', centerX + 10, centerY - 2 * scale - 10, 12, COLORS.orange, 'left'); +function drawSplitComparison() { + const canvas = document.getElementById('split-comparison'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const splits = [ + { name: 'Split A: "Contains FREE"', ig: 0.034, color: '#ff8c6a' }, + { name: 'Split B: "Has Link"', ig: 0.156, color: '#7ef0d4' }, + { name: 'Split C: "Urgent"', ig: 0.089, color: '#ffb490' } + ]; + + const padding = 60; + const barHeight = 60; + const maxWidth = width - 2 * padding - 200; + const maxIG = Math.max(...splits.map(s => s.ig)); + + splits.forEach((split, i) => { + const y = 80 + i * (barHeight + 40); + const barWidth = (split.ig / maxIG) * maxWidth; + + // Bar + ctx.fillStyle = split.color; + ctx.fillRect(padding, y, barWidth, barHeight); + + // Border + ctx.strokeStyle = split.color; + ctx.lineWidth = 2; + ctx.strokeRect(padding, y, barWidth, barHeight); + + // Label + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 13px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(split.name, padding, y - 10); + + // Value + ctx.fillStyle = '#1a2332'; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(`IG = ${split.ig.toFixed(3)}`, padding + barWidth / 2, y + barHeight / 2 + 6); + }); + + // Winner + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('✓ Best split: Highest Information Gain!', width / 2, height - 30); + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.fillText('Comparing Split Quality', width / 2, 40); +} + +function drawEntropyViz() { + const canvas = document.getElementById('entropy-viz'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + // Draw entropy curve + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 3; + ctx.beginPath(); + for (let p = 0.01; p <= 0.99; p += 0.01) { + const entropy = -p * Math.log2(p) - (1 - p) * Math.log2(1 - p); + const x = padding + p * chartWidth; + const y = height - padding - entropy * chartHeight; + if (p === 0.01) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.stroke(); + + // Mark key points + const points = [ + { p: 0.1, label: 'Pure\n(low)' }, + { p: 0.5, label: 'Maximum\n(high)' }, + { p: 0.9, label: 'Pure\n(low)' } + ]; + + points.forEach(point => { + const entropy = -point.p * Math.log2(point.p) - (1 - point.p) * Math.log2(1 - point.p); + const x = padding + point.p * chartWidth; + const y = height - padding - entropy * chartHeight; + + ctx.fillStyle = '#7ef0d4'; + ctx.beginPath(); + ctx.arc(x, y, 6, 0, 2 * Math.PI); + ctx.fill(); + + ctx.fillStyle = '#7ef0d4'; + ctx.font = '11px sans-serif'; + ctx.textAlign = 'center'; + const lines = point.label.split('\n'); + lines.forEach((line, i) => { + ctx.fillText(line, x, y - 15 - (lines.length - 1 - i) * 12); + }); + }); + + // Axes + ctx.strokeStyle = '#2a3544'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(padding, padding); + ctx.lineTo(padding, height - padding); + ctx.lineTo(width - padding, height - padding); + ctx.stroke(); + + // Labels + ctx.fillStyle = '#a9b4c2'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Proportion of Positive Class (p)', width / 2, height - 20); + ctx.save(); + ctx.translate(20, height / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('Entropy H(p)', 0, 0); + ctx.restore(); + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Entropy: Measuring Disorder', width / 2, 30); +} + +function drawSplitComparison() { + const canvas = document.getElementById('split-comparison'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const splits = [ + { name: 'Split A: "Contains FREE"', ig: 0.034, color: '#ff8c6a' }, + { name: 'Split B: "Has Link"', ig: 0.156, color: '#7ef0d4' }, + { name: 'Split C: "Urgent"', ig: 0.089, color: '#ffb490' } + ]; + + const padding = 60; + const barHeight = 60; + const maxWidth = width - 2 * padding - 200; + const maxIG = Math.max(...splits.map(s => s.ig)); + + splits.forEach((split, i) => { + const y = 80 + i * (barHeight + 40); + const barWidth = (split.ig / maxIG) * maxWidth; + + // Bar + ctx.fillStyle = split.color; + ctx.fillRect(padding, y, barWidth, barHeight); + + // Border + ctx.strokeStyle = split.color; + ctx.lineWidth = 2; + ctx.strokeRect(padding, y, barWidth, barHeight); + + // Label + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 13px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(split.name, padding, y - 10); + + // Value + ctx.fillStyle = '#1a2332'; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(`IG = ${split.ig.toFixed(3)}`, padding + barWidth / 2, y + barHeight / 2 + 6); + }); + + // Winner + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('✓ Best split: Highest Information Gain!', width / 2, height - 30); + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.fillText('Comparing Split Quality', width / 2, 40); +} + +function drawEntropyViz() { + const canvas = document.getElementById('entropy-viz'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + // Draw entropy curve + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 3; + ctx.beginPath(); + for (let p = 0.01; p <= 0.99; p += 0.01) { + const entropy = -p * Math.log2(p) - (1 - p) * Math.log2(1 - p); + const x = padding + p * chartWidth; + const y = height - padding - entropy * chartHeight; + if (p === 0.01) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.stroke(); + + // Mark key points + const points = [ + { p: 0.1, label: 'Pure\n(low)' }, + { p: 0.5, label: 'Maximum\n(high)' }, + { p: 0.9, label: 'Pure\n(low)' } + ]; + + points.forEach(point => { + const entropy = -point.p * Math.log2(point.p) - (1 - point.p) * Math.log2(1 - point.p); + const x = padding + point.p * chartWidth; + const y = height - padding - entropy * chartHeight; + + ctx.fillStyle = '#7ef0d4'; + ctx.beginPath(); + ctx.arc(x, y, 6, 0, 2 * Math.PI); + ctx.fill(); + + ctx.fillStyle = '#7ef0d4'; + ctx.font = '11px sans-serif'; + ctx.textAlign = 'center'; + const lines = point.label.split('\n'); + lines.forEach((line, i) => { + ctx.fillText(line, x, y - 15 - (lines.length - 1 - i) * 12); + }); + }); + + // Axes + ctx.strokeStyle = '#2a3544'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(padding, padding); + ctx.lineTo(padding, height - padding); + ctx.lineTo(width - padding, height - padding); + ctx.stroke(); + + // Labels + ctx.fillStyle = '#a9b4c2'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Proportion of Positive Class (p)', width / 2, height - 20); + ctx.save(); + ctx.translate(20, height / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('Entropy H(p)', 0, 0); + ctx.restore(); + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Entropy: Measuring Disorder', width / 2, 30); +} + +function drawTreeBoundary() { + const canvas = document.getElementById('tree-boundary'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + // Draw regions + const regions = [ + { x1: 0, y1: 0, x2: 0.5, y2: 0.6, class: 'orange' }, + { x1: 0.5, y1: 0, x2: 1, y2: 0.6, class: 'yellow' }, + { x1: 0, y1: 0.6, x2: 0.3, y2: 1, class: 'yellow' }, + { x1: 0.3, y1: 0.6, x2: 1, y2: 1, class: 'orange' } + ]; + + regions.forEach(region => { + const x = padding + region.x1 * chartWidth; + const y = padding + region.y1 * chartHeight; + const w = (region.x2 - region.x1) * chartWidth; + const h = (region.y2 - region.y1) * chartHeight; + + ctx.fillStyle = region.class === 'orange' ? 'rgba(255, 140, 106, 0.2)' : 'rgba(255, 235, 59, 0.2)'; + ctx.fillRect(x, y, w, h); + + ctx.strokeStyle = region.class === 'orange' ? '#ff8c6a' : '#ffeb3b'; + ctx.lineWidth = 2; + ctx.strokeRect(x, y, w, h); + }); + + // Generate random points + const orangePoints = []; + const yellowPoints = []; + for (let i = 0; i < 15; i++) { + if (Math.random() < 0.3) { + orangePoints.push({ x: Math.random() * 0.5, y: Math.random() * 0.6 }); } - - const transformBtn = document.getElementById('btn54transform'); - const resetBtn = document.getElementById('btn54reset'); - - if (transformBtn) { - transformBtn.addEventListener('click', () => { - transformed = true; - draw(); - }); + if (Math.random() < 0.3) { + yellowPoints.push({ x: 0.5 + Math.random() * 0.5, y: Math.random() * 0.6 }); } - - if (resetBtn) { - resetBtn.addEventListener('click', () => { - transformed = false; - draw(); - }); + if (Math.random() < 0.3) { + orangePoints.push({ x: 0.3 + Math.random() * 0.7, y: 0.6 + Math.random() * 0.4 }); + } + if (Math.random() < 0.3) { + yellowPoints.push({ x: Math.random() * 0.3, y: 0.6 + Math.random() * 0.4 }); } + } + + // Draw points + orangePoints.forEach(p => { + ctx.fillStyle = '#ff8c6a'; + ctx.beginPath(); + ctx.arc(padding + p.x * chartWidth, padding + p.y * chartHeight, 5, 0, 2 * Math.PI); + ctx.fill(); + }); + + yellowPoints.forEach(p => { + ctx.fillStyle = '#ffeb3b'; + ctx.beginPath(); + ctx.arc(padding + p.x * chartWidth, padding + p.y * chartHeight, 5, 0, 2 * Math.PI); + ctx.fill(); + }); + + // Labels + ctx.fillStyle = '#a9b4c2'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Feature 1', width / 2, height - 20); + ctx.save(); + ctx.translate(20, height / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('Feature 2', 0, 0); + ctx.restore(); + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Decision Tree Creates Rectangular Regions', width / 2, 30); +} + +// Topic 17: Ensemble Methods +function initEnsembleMethods() { + const canvas1 = document.getElementById('bagging-viz'); + if (canvas1 && !canvas1.dataset.initialized) { + canvas1.dataset.initialized = 'true'; + drawBaggingViz(); + } + + const canvas2 = document.getElementById('boosting-viz'); + if (canvas2 && !canvas2.dataset.initialized) { + canvas2.dataset.initialized = 'true'; + drawBoostingViz(); + } + + const canvas3 = document.getElementById('random-forest-viz'); + if (canvas3 && !canvas3.dataset.initialized) { + canvas3.dataset.initialized = 'true'; + drawRandomForestViz(); + } +} + +function drawBaggingViz() { + const canvas = document.getElementById('bagging-viz'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const boxWidth = 150; + const boxHeight = 60; + const startY = 60; + const spacing = (width - 3 * boxWidth) / 4; + + // Original data + ctx.fillStyle = '#6aa9ff33'; + ctx.fillRect(width / 2 - 100, startY, 200, boxHeight); + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 2; + ctx.strokeRect(width / 2 - 100, startY, 200, boxHeight); + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Original Dataset', width / 2, startY + boxHeight / 2 + 5); + + // Bootstrap samples + const sampleY = startY + boxHeight + 60; + for (let i = 0; i < 3; i++) { + const x = spacing + i * (boxWidth + spacing); + + // Arrow + ctx.strokeStyle = '#7ef0d4'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(width / 2, startY + boxHeight); + ctx.lineTo(x + boxWidth / 2, sampleY); + ctx.stroke(); - draw(); + // Sample box + ctx.fillStyle = '#7ef0d433'; + ctx.fillRect(x, sampleY, boxWidth, boxHeight); + ctx.strokeStyle = '#7ef0d4'; + ctx.strokeRect(x, sampleY, boxWidth, boxHeight); + + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 12px sans-serif'; + ctx.fillText(`Bootstrap ${i + 1}`, x + boxWidth / 2, sampleY + boxHeight / 2 - 5); + ctx.font = '10px sans-serif'; + ctx.fillStyle = '#a9b4c2'; + ctx.fillText('(random sample)', x + boxWidth / 2, sampleY + boxHeight / 2 + 10); + + // Model + const modelY = sampleY + boxHeight + 40; + ctx.fillStyle = '#ffb49033'; + ctx.fillRect(x, modelY, boxWidth, boxHeight); + ctx.strokeStyle = '#ffb490'; + ctx.strokeRect(x, modelY, boxWidth, boxHeight); + + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 12px sans-serif'; + ctx.fillText(`Model ${i + 1}`, x + boxWidth / 2, modelY + boxHeight / 2 + 5); + + // Arrow to final + ctx.strokeStyle = '#ffb490'; + ctx.beginPath(); + ctx.moveTo(x + boxWidth / 2, modelY + boxHeight); + ctx.lineTo(width / 2, height - 60); + ctx.stroke(); + } + + // Final prediction + ctx.fillStyle = '#ff8c6a33'; + ctx.fillRect(width / 2 - 100, height - 60, 200, boxHeight); + ctx.strokeStyle = '#ff8c6a'; + ctx.lineWidth = 3; + ctx.strokeRect(width / 2 - 100, height - 60, 200, boxHeight); + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 14px sans-serif'; + ctx.fillText('Average / Vote', width / 2, height - 60 + boxHeight / 2 + 5); + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.fillText('Bagging: Bootstrap Aggregating', width / 2, 30); } -// ===== CALCULUS VISUALIZATIONS ===== -function initCircleAreaCanvas() { - const canvas = document.getElementById('canvas-58'); - if (!canvas) return; + +function drawBoostingViz() { + const canvas = document.getElementById('boosting-viz'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 450; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const iterY = [80, 180, 280]; + const dataX = 100; + const modelX = width / 2; + const predX = width - 150; + + for (let i = 0; i < 3; i++) { + const y = iterY[i]; + const alpha = i === 0 ? 1 : (i === 1 ? 0.7 : 0.5); + + // Iteration label + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText(`Iteration ${i + 1}`, 20, y + 30); + + // Data with weights + ctx.globalAlpha = alpha; + ctx.fillStyle = '#6aa9ff33'; + ctx.fillRect(dataX, y, 120, 60); + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 2; + ctx.strokeRect(dataX, y, 120, 60); + ctx.globalAlpha = 1; + + ctx.fillStyle = '#e8eef6'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Weighted Data', dataX + 60, y + 25); + ctx.fillStyle = i > 0 ? '#ff8c6a' : '#7ef0d4'; + ctx.font = 'bold 11px sans-serif'; + ctx.fillText(i > 0 ? '↑ Focus on errors' : 'Equal weights', dataX + 60, y + 45); + + // Arrow + ctx.strokeStyle = '#7ef0d4'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(dataX + 120, y + 30); + ctx.lineTo(modelX - 60, y + 30); + ctx.stroke(); - const ctx = canvas.getContext('2d'); - let unwrapping = false; - let progress = 0; + // Model + ctx.fillStyle = '#ffb49033'; + ctx.fillRect(modelX - 60, y, 120, 60); + ctx.strokeStyle = '#ffb490'; + ctx.strokeRect(modelX - 60, y, 120, 60); - function draw() { - clearCanvas(ctx, canvas); - - const centerX = canvas.width / 4; - const centerY = canvas.height / 2; - const radius = 100; - - if (!unwrapping) { - // Draw circle - drawCircle(ctx, centerX, centerY, radius, COLORS.cyan, false); - drawText(ctx, 'Circle: Area = πr²', centerX, centerY + radius + 30, 14, COLORS.cyan); - } else { - // Draw unwrapped rings - const rings = 20; - for (let i = 0; i < rings * progress; i++) { - const r = (i / rings) * radius; - const rectX = canvas.width / 2 + i * 3; - const rectY = centerY - r; - const rectHeight = 2 * r; - const dr = radius / rings; - - ctx.fillStyle = COLORS.cyan; - ctx.globalAlpha = 0.6; - ctx.fillRect(rectX, rectY, 2, rectHeight); - } - ctx.globalAlpha = 1; - - if (progress >= 0.99) { - drawText(ctx, 'Integrated! Area = πr²', canvas.width * 0.7, centerY + 50, 14, COLORS.green); - } - } - } + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 12px sans-serif'; + ctx.fillText(`Model ${i + 1}`, modelX, y + 35); - const animateBtn = document.getElementById('btn58animate'); - const resetBtn = document.getElementById('btn58reset'); - - if (animateBtn) { - animateBtn.addEventListener('click', () => { - unwrapping = true; - progress = 0; - const interval = setInterval(() => { - progress += 0.02; - draw(); - if (progress >= 1) { - clearInterval(interval); - } - }, 50); - }); - } + // Arrow + ctx.strokeStyle = '#ffb490'; + ctx.beginPath(); + ctx.moveTo(modelX + 60, y + 30); + ctx.lineTo(predX - 60, y + 30); + ctx.stroke(); - if (resetBtn) { - resetBtn.addEventListener('click', () => { - unwrapping = false; - progress = 0; - draw(); - }); + // Predictions + ctx.fillStyle = '#7ef0d433'; + ctx.fillRect(predX - 60, y, 120, 60); + ctx.strokeStyle = '#7ef0d4'; + ctx.strokeRect(predX - 60, y, 120, 60); + + ctx.fillStyle = '#e8eef6'; + ctx.font = '11px sans-serif'; + ctx.fillText('Predictions', predX, y + 25); + ctx.fillStyle = i < 2 ? '#ff8c6a' : '#7ef0d4'; + ctx.font = 'bold 10px sans-serif'; + ctx.fillText(i < 2 ? 'Some errors' : 'Better!', predX, y + 45); + + // Feedback arrow + if (i < 2) { + ctx.strokeStyle = '#ff8c6a'; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + ctx.beginPath(); + ctx.moveTo(predX - 60, y + 60); + ctx.lineTo(dataX + 60, y + 90); + ctx.stroke(); + ctx.setLineDash([]); + + ctx.fillStyle = '#ff8c6a'; + ctx.font = '10px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Increase weights for errors', width / 2, y + 80); } - - draw(); + } + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Boosting: Sequential Learning from Mistakes', width / 2, 30); + + // Final + ctx.fillStyle = '#ff8c6a'; + ctx.font = 'bold 14px sans-serif'; + ctx.fillText('Final Prediction = Weighted Combination of All Models', width / 2, height - 20); } -function initDerivativeCanvas() { - const canvas = document.getElementById('canvas-59'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let dx = 1.0; - - function f(x) { - return 0.02 * x * x; - } +function drawRandomForestViz() { + const canvas = document.getElementById('random-forest-viz'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 400; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const treeY = 120; + const numTrees = 5; + const treeSpacing = (width - 100) / numTrees; + const treeSize = 50; + + // Original data + ctx.fillStyle = '#6aa9ff33'; + ctx.fillRect(width / 2 - 100, 40, 200, 50); + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 2; + ctx.strokeRect(width / 2 - 100, 40, 200, 50); + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Training Data', width / 2, 70); + + // Trees + for (let i = 0; i < numTrees; i++) { + const x = 50 + i * treeSpacing + treeSpacing / 2; + + // Arrow from data + ctx.strokeStyle = '#7ef0d4'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(width / 2, 90); + ctx.lineTo(x, treeY - 20); + ctx.stroke(); - function draw() { - clearCanvas(ctx, canvas); - - const scale = 50; - const centerX = canvas.width / 2; - const centerY = canvas.height * 0.8; - - // Draw axes - drawLine(ctx, 50, centerY, canvas.width - 50, centerY, '#555', 1); - drawLine(ctx, centerX, 50, centerX, canvas.height - 50, '#555', 1); - - // Draw function - ctx.beginPath(); - for (let x = -canvas.width / 2; x < canvas.width / 2; x += 2) { - const px = centerX + x; - const py = centerY - f(x) * scale; - if (x === -canvas.width / 2) { - ctx.moveTo(px, py); - } else { - ctx.lineTo(px, py); - } - } - ctx.strokeStyle = COLORS.cyan; - ctx.lineWidth = 2; - ctx.stroke(); - - // Draw secant line - const x0 = 0; - const x1 = dx * scale; - const y0 = centerY - f(x0) * scale; - const y1 = centerY - f(x1) * scale; - - drawLine(ctx, centerX + x0 - 50, y0 - (y1 - y0) / x1 * 50, centerX + x1 + 50, y1 + (y1 - y0) / x1 * 50, COLORS.orange, 2); - drawCircle(ctx, centerX + x0, y0, 5, COLORS.green); - drawCircle(ctx, centerX + x1, y1, 5, COLORS.green); - - const slope = (f(x1 / scale) - f(0)) / (x1 / scale); - drawText(ctx, `Slope = ${slope.toFixed(3)}`, canvas.width / 2, 40, 14, COLORS.orange); - drawText(ctx, `As Δx → 0, slope → derivative`, canvas.width / 2, 60, 12, COLORS.text); + // Tree icon (triangle) + ctx.fillStyle = '#7ef0d4'; + ctx.beginPath(); + ctx.moveTo(x, treeY - 20); + ctx.lineTo(x - treeSize / 2, treeY + treeSize - 20); + ctx.lineTo(x + treeSize / 2, treeY + treeSize - 20); + ctx.closePath(); + ctx.fill(); + + // Trunk + ctx.fillStyle = '#ffb490'; + ctx.fillRect(x - 8, treeY + treeSize - 20, 16, 30); + + // Tree label + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 11px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(`Tree ${i + 1}`, x, treeY + treeSize + 25); + + // Random features note + if (i === 0) { + ctx.font = '9px sans-serif'; + ctx.fillStyle = '#a9b4c2'; + ctx.fillText('Random', x, treeY + treeSize + 40); + ctx.fillText('subset', x, treeY + treeSize + 52); } - const slider = document.getElementById('slider59dx'); - const label = document.getElementById('label59dx'); + // Prediction + const predY = treeY + treeSize + 70; + ctx.fillStyle = i < 3 ? '#ff8c6a' : '#7ef0d4'; + ctx.beginPath(); + ctx.arc(x, predY, 12, 0, 2 * Math.PI); + ctx.fill(); - if (slider) { - slider.addEventListener('input', (e) => { - dx = parseFloat(e.target.value); - if (label) label.textContent = dx.toFixed(1); - draw(); - }); - } + ctx.fillStyle = '#1a2332'; + ctx.font = 'bold 10px sans-serif'; + ctx.fillText(i < 3 ? '1' : '0', x, predY + 4); - draw(); + // Arrow to vote + ctx.strokeStyle = i < 3 ? '#ff8c6a' : '#7ef0d4'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(x, predY + 12); + ctx.lineTo(width / 2, height - 80); + ctx.stroke(); + } + + // Vote box + ctx.fillStyle = '#7ef0d433'; + ctx.fillRect(width / 2 - 80, height - 80, 160, 60); + ctx.strokeStyle = '#7ef0d4'; + ctx.lineWidth = 3; + ctx.strokeRect(width / 2 - 80, height - 80, 160, 60); + + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Majority Vote', width / 2, height - 60); + ctx.font = 'bold 16px sans-serif'; + ctx.fillStyle = '#ff8c6a'; + ctx.fillText('Class 1 wins (3 vs 2)', width / 2, height - 35); + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.fillText('Random Forest: Ensemble of Decision Trees', width / 2, 25); } -function initRiemannSumCanvas() { - const canvas = document.getElementById('canvas-64'); - if (!canvas) return; +// Topic 16: K-means Clustering +let kmeansVizChart = null; +let kmeansElbowChart = null; + +function initKMeans() { + const canvas1 = document.getElementById('kmeans-viz-canvas'); + if (canvas1 && !canvas1.dataset.initialized) { + canvas1.dataset.initialized = 'true'; + drawKMeansVisualization(); + } + + const canvas2 = document.getElementById('kmeans-elbow-canvas'); + if (canvas2 && !canvas2.dataset.initialized) { + canvas2.dataset.initialized = 'true'; + drawKMeansElbow(); + } +} + +function drawKMeansVisualization() { + const canvas = document.getElementById('kmeans-viz-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 450; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const padding = 60; + const chartWidth = width - 2 * padding; + const chartHeight = height - 2 * padding; + + const xMin = 0, xMax = 10, yMin = 0, yMax = 12; + const scaleX = (x) => padding + (x / xMax) * chartWidth; + const scaleY = (y) => height - padding - (y / yMax) * chartHeight; + + // Data points + const points = [ + {id: 'A', x: 1, y: 2, cluster: 1}, + {id: 'B', x: 1.5, y: 1.8, cluster: 1}, + {id: 'C', x: 5, y: 8, cluster: 2}, + {id: 'D', x: 8, y: 8, cluster: 2}, + {id: 'E', x: 1, y: 0.6, cluster: 1}, + {id: 'F', x: 9, y: 11, cluster: 2} + ]; + + // Final centroids + const centroids = [ + {x: 1.17, y: 1.47, color: '#7ef0d4'}, + {x: 7.33, y: 9.0, color: '#ff8c6a'} + ]; + + // Draw lines from points to centroids + points.forEach(p => { + const c = centroids[p.cluster - 1]; + ctx.strokeStyle = p.cluster === 1 ? 'rgba(126, 240, 212, 0.3)' : 'rgba(255, 140, 106, 0.3)'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(scaleX(p.x), scaleY(p.y)); + ctx.lineTo(scaleX(c.x), scaleY(c.y)); + ctx.stroke(); + }); + + // Draw points + points.forEach(p => { + ctx.fillStyle = p.cluster === 1 ? '#7ef0d4' : '#ff8c6a'; + ctx.beginPath(); + ctx.arc(scaleX(p.x), scaleY(p.y), 8, 0, 2 * Math.PI); + ctx.fill(); + ctx.strokeStyle = '#1a2332'; + ctx.lineWidth = 2; + ctx.stroke(); - const ctx = canvas.getContext('2d'); - let n = 8; + // Label + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(p.id, scaleX(p.x), scaleY(p.y) - 15); + }); + + // Draw centroids + centroids.forEach((c, i) => { + ctx.fillStyle = c.color; + ctx.beginPath(); + ctx.arc(scaleX(c.x), scaleY(c.y), 12, 0, 2 * Math.PI); + ctx.fill(); + ctx.strokeStyle = '#e8eef6'; + ctx.lineWidth = 3; + ctx.stroke(); - function f(x) { - return 50 + 50 * Math.sin(x / 30); - } + // Draw X + ctx.strokeStyle = '#1a2332'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(scaleX(c.x) - 6, scaleY(c.y) - 6); + ctx.lineTo(scaleX(c.x) + 6, scaleY(c.y) + 6); + ctx.moveTo(scaleX(c.x) + 6, scaleY(c.y) - 6); + ctx.lineTo(scaleX(c.x) - 6, scaleY(c.y) + 6); + ctx.stroke(); - function draw() { - clearCanvas(ctx, canvas); - - const padding = 50; - const width = canvas.width - 2 * padding; - const height = canvas.height - 2 * padding; - - // Draw axes - drawLine(ctx, padding, height + padding, canvas.width - padding, height + padding, '#555', 2); - drawLine(ctx, padding, padding, padding, height + padding, '#555', 2); - - // Draw function - ctx.beginPath(); - for (let x = 0; x <= width; x += 2) { - const px = padding + x; - const py = height + padding - f(x); - if (x === 0) { - ctx.moveTo(px, py); - } else { - ctx.lineTo(px, py); + // Label + ctx.fillStyle = '#e8eef6'; + ctx.font = 'bold 13px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(`c${i+1}`, scaleX(c.x), scaleY(c.y) + 25); + }); + + // Axes + ctx.strokeStyle = '#2a3544'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(padding, padding); + ctx.lineTo(padding, height - padding); + ctx.lineTo(width - padding, height - padding); + ctx.stroke(); + + // Labels + ctx.fillStyle = '#a9b4c2'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('X', width / 2, height - 20); + ctx.save(); + ctx.translate(20, height / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillText('Y', 0, 0); + ctx.restore(); + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('K-means Clustering (K=2) - Final State', width / 2, 30); + + // WCSS + ctx.fillStyle = '#6aa9ff'; + ctx.font = '14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('WCSS = 15.984', padding, height - padding + 30); +} + +function drawKMeansElbow() { + const canvas = document.getElementById('kmeans-elbow-canvas'); + if (!canvas) return; + + if (kmeansElbowChart) { + kmeansElbowChart.destroy(); + } + + const ctx = canvas.getContext('2d'); + + const kValues = [1, 2, 3, 4, 5]; + const wcssValues = [50, 18, 10, 8, 7]; + + kmeansElbowChart = safeCreateChart(ctx, { + type: 'line', + data: { + labels: kValues, + datasets: [{ + label: 'WCSS', + data: wcssValues, + borderColor: '#6aa9ff', + backgroundColor: 'rgba(106, 169, 255, 0.1)', + borderWidth: 3, + fill: true, + tension: 0.4, + pointRadius: kValues.map(k => k === 3 ? 10 : 6), + pointBackgroundColor: kValues.map(k => k === 3 ? '#7ef0d4' : '#6aa9ff'), + pointBorderColor: kValues.map(k => k === 3 ? '#7ef0d4' : '#6aa9ff'), + pointBorderWidth: kValues.map(k => k === 3 ? 3 : 2) + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { + display: true, + text: 'Elbow Method: Optimal K = 3 (Elbow Point)', + color: '#7ef0d4', + font: { size: 16, weight: 'bold' } + }, + legend: { + labels: { color: '#a9b4c2' } + }, + annotation: { + annotations: { + line1: { + type: 'line', + xMin: 3, + xMax: 3, + borderColor: '#7ef0d4', + borderWidth: 2, + borderDash: [5, 5], + label: { + display: true, + content: 'Elbow!', + position: 'start' + } } + } } - ctx.strokeStyle = COLORS.cyan; - ctx.lineWidth = 3; - ctx.stroke(); - - // Draw rectangles - const rectWidth = width / n; - let totalArea = 0; - - for (let i = 0; i < n; i++) { - const x = i * rectWidth; - const rectHeight = f(x); - totalArea += rectWidth * rectHeight; - - ctx.fillStyle = COLORS.orange; - ctx.globalAlpha = 0.5; - ctx.fillRect(padding + x, height + padding - rectHeight, rectWidth, rectHeight); - ctx.strokeStyle = COLORS.orange; - ctx.globalAlpha = 1; - ctx.strokeRect(padding + x, height + padding - rectHeight, rectWidth, rectHeight); + }, + scales: { + x: { + title: { display: true, text: 'Number of Clusters (K)', color: '#a9b4c2' }, + grid: { color: '#2a3544' }, + ticks: { color: '#a9b4c2', stepSize: 1 } + }, + y: { + title: { display: true, text: 'Within-Cluster Sum of Squares (WCSS)', color: '#a9b4c2' }, + grid: { color: '#2a3544' }, + ticks: { color: '#a9b4c2' }, + min: 0 } - - drawText(ctx, `Rectangles: ${n}`, canvas.width / 2, 30, 14, COLORS.text); - drawText(ctx, `Approximate Area: ${(totalArea / 100).toFixed(2)}`, canvas.width / 2, 50, 12, COLORS.orange); + } } - - const slider = document.getElementById('slider64n'); - const label = document.getElementById('label64n'); - - if (slider) { - slider.addEventListener('input', (e) => { - n = parseInt(e.target.value); - if (label) label.textContent = n; - draw(); - }); + }, 'K-means Elbow Chart'); +} + +// Topic 18: Algorithm Comparison +let comparisonState = { + selectedAlgorithms: [], + algorithmData: { + 'Linear Regression': { + category: 'Supervised - Regression', + speed: 5, accuracy: 3, dataRequired: 1, interpretability: 5, scalability: 3, + featureScaling: 'Required', nonLinear: 'No', trainingTime: 'Fast', memoryUsage: 'Low', + bestFor: 'Linear trends, forecasting', + pros: ['Very fast', 'Highly interpretable', 'Works with little data', 'No tuning needed'], + cons: ['Assumes linearity', 'Sensitive to outliers', 'No complex patterns'], + useCases: { regression: 5, classification: 0, clustering: 0, speed: 5, interpretability: 5 } + }, + 'Logistic Regression': { + category: 'Supervised - Classification', + speed: 5, accuracy: 4, dataRequired: 2, interpretability: 4, scalability: 4, + featureScaling: 'Required', nonLinear: 'No', trainingTime: 'Fast', memoryUsage: 'Low', + bestFor: 'Binary classification, probabilities', + pros: ['Fast', 'Probabilistic output', 'Interpretable', 'Works well'], + cons: ['Binary only', 'Assumes linearity', 'Limited complexity'], + useCases: { regression: 0, classification: 5, clustering: 0, speed: 5, interpretability: 4 } + }, + 'SVM': { + category: 'Supervised - Classification', + speed: 2, accuracy: 5, dataRequired: 2, interpretability: 2, scalability: 2, + featureScaling: 'Required', nonLinear: 'Yes', trainingTime: 'Slow', memoryUsage: 'Medium', + bestFor: 'High accuracy, complex boundaries', + pros: ['Very high accuracy', 'Handles non-linear', 'Effective in high dims'], + cons: ['Slow training', 'Hard to interpret', 'Needs tuning'], + useCases: { regression: 2, classification: 5, clustering: 0, speed: 2, interpretability: 2 } + }, + 'KNN': { + category: 'Supervised - Classification', + speed: 1, accuracy: 4, dataRequired: 3, interpretability: 3, scalability: 1, + featureScaling: 'Required', nonLinear: 'Yes', trainingTime: 'None', memoryUsage: 'High', + bestFor: 'Local patterns, small datasets', + pros: ['Simple', 'No training', 'Handles non-linear'], + cons: ['Very slow prediction', 'Needs lots of memory', 'Needs scaling'], + useCases: { regression: 3, classification: 4, clustering: 3, speed: 1, interpretability: 3 } + }, + 'Naive Bayes': { + category: 'Supervised - Classification', + speed: 5, accuracy: 3, dataRequired: 2, interpretability: 5, scalability: 5, + featureScaling: 'Not needed', nonLinear: 'Yes', trainingTime: 'Fast', memoryUsage: 'Low', + bestFor: 'Quick models, text classification', + pros: ['Very fast', 'Interpretable', 'Works with little data'], + cons: ['Independence assumption wrong', 'Often biased', 'Limited accuracy'], + useCases: { regression: 2, classification: 4, clustering: 0, speed: 5, interpretability: 5 } + }, + 'Decision Trees': { + category: 'Supervised - Classification', + speed: 3, accuracy: 4, dataRequired: 2, interpretability: 5, scalability: 3, + featureScaling: 'Not needed', nonLinear: 'Yes', trainingTime: 'Medium', memoryUsage: 'Low', + bestFor: 'Interpretability, complex decisions', + pros: ['Very interpretable', 'No scaling needed', 'Handles non-linear'], + cons: ['Prone to overfitting', 'Unstable', 'Biased to dominant class'], + useCases: { regression: 3, classification: 4, clustering: 0, speed: 3, interpretability: 5 } + }, + 'Random Forest': { + category: 'Supervised - Classification', + speed: 2, accuracy: 5, dataRequired: 3, interpretability: 3, scalability: 3, + featureScaling: 'Not needed', nonLinear: 'Yes', trainingTime: 'Slow', memoryUsage: 'Medium', + bestFor: 'High accuracy with complex data', + pros: ['Very high accuracy', 'No scaling', 'Handles non-linear'], + cons: ['Slow', 'Less interpretable', 'Black box'], + useCases: { regression: 3, classification: 5, clustering: 0, speed: 3, interpretability: 2 } + }, + 'K-means': { + category: 'Unsupervised - Clustering', + speed: 4, accuracy: 3, dataRequired: 3, interpretability: 4, scalability: 4, + featureScaling: 'Required', nonLinear: 'No', trainingTime: 'Medium', memoryUsage: 'Low', + bestFor: 'Customer segmentation, grouping', + pros: ['Fast', 'Simple', 'Scalable'], + cons: ['Need to specify K', 'Sensitive to init', 'Assumes spherical'], + useCases: { regression: 0, classification: 0, clustering: 5, speed: 4, interpretability: 4 } + }, + 'PCA': { + category: 'Unsupervised - Dimensionality Reduction', + speed: 3, accuracy: 4, dataRequired: 2, interpretability: 2, scalability: 4, + featureScaling: 'Required', nonLinear: 'No', trainingTime: 'Medium', memoryUsage: 'Medium', + bestFor: 'High-dimensional data reduction', + pros: ['Reduces dimensions', 'Preserves variance', 'Fast after trained'], + cons: ['Components not interpretable', 'Linear only', 'Assumes normality'], + useCases: { regression: 0, classification: 0, clustering: 0, speed: 4, interpretability: 2 } } - - draw(); + } +}; + +function initAlgorithmComparison() { + const container = document.getElementById('algorithm-checkboxes'); + if (!container || container.dataset.initialized) return; + container.dataset.initialized = 'true'; + + populateAlgorithmCheckboxes(); + initComparisonListeners(); + initQuiz(); } -function initTaylorSeriesCanvas() { - const canvas = document.getElementById('canvas-68'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let degree = 1; - let func = 'sin'; - - function draw() { - clearCanvas(ctx, canvas); +function populateAlgorithmCheckboxes() { + const container = document.getElementById('algorithm-checkboxes'); + if (!container) return; + + const categoryRadios = document.querySelectorAll('input[name="category"]'); + + function updateCheckboxes() { + const selectedCategory = document.querySelector('input[name="category"]:checked')?.value || 'all'; + container.innerHTML = ''; + + Object.keys(comparisonState.algorithmData).forEach(name => { + const algo = comparisonState.algorithmData[name]; + const category = algo.category.toLowerCase(); + + if (selectedCategory === 'all' || + (selectedCategory === 'supervised' && category.includes('supervised')) || + (selectedCategory === 'unsupervised' && category.includes('unsupervised'))) { - const scale = 50; - const centerX = canvas.width / 2; - const centerY = canvas.height / 2; + const label = document.createElement('label'); + label.style.display = 'flex'; + label.style.alignItems = 'center'; + label.style.gap = '8px'; + label.style.cursor = 'pointer'; + label.style.padding = '8px'; + label.style.borderRadius = '6px'; + label.style.transition = 'background 0.2s'; - // Draw axes - drawLine(ctx, 50, centerY, canvas.width - 50, centerY, '#555', 1); - drawLine(ctx, centerX, 50, centerX, canvas.height - 50, '#555', 1); + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.value = name; + checkbox.addEventListener('change', updateSelection); - // Draw actual function - ctx.beginPath(); - for (let x = -canvas.width / 2; x < canvas.width / 2; x += 2) { - const t = x / scale; - let y; - if (func === 'sin') y = Math.sin(t); - else if (func === 'cos') y = Math.cos(t); - else y = Math.exp(t); - - const px = centerX + x; - const py = centerY - y * scale; - if (x === -canvas.width / 2) { - ctx.moveTo(px, py); - } else { - ctx.lineTo(px, py); - } - } - ctx.strokeStyle = COLORS.cyan; - ctx.lineWidth = 2; - ctx.stroke(); + const text = document.createTextNode(name); - // Draw Taylor approximation - ctx.beginPath(); - for (let x = -canvas.width / 2; x < canvas.width / 2; x += 2) { - const t = x / scale; - let y = 0; - - if (func === 'sin') { - for (let n = 0; n <= degree; n++) { - const term = Math.pow(-1, n) * Math.pow(t, 2 * n + 1) / factorial(2 * n + 1); - y += term; - } - } else if (func === 'cos') { - for (let n = 0; n <= degree; n++) { - const term = Math.pow(-1, n) * Math.pow(t, 2 * n) / factorial(2 * n); - y += term; - } - } else { - for (let n = 0; n <= degree; n++) { - y += Math.pow(t, n) / factorial(n); - } - } - - const px = centerX + x; - const py = centerY - y * scale; - if (x === -canvas.width / 2) { - ctx.moveTo(px, py); - } else { - ctx.lineTo(px, py); - } - } - ctx.strokeStyle = COLORS.orange; - ctx.lineWidth = 3; - ctx.stroke(); + label.appendChild(checkbox); + label.appendChild(text); + label.addEventListener('mouseenter', () => label.style.background = 'var(--color-secondary)'); + label.addEventListener('mouseleave', () => label.style.background = 'transparent'); - drawText(ctx, `Function: ${func}(x)`, canvas.width / 2, 30, 14, COLORS.cyan); - drawText(ctx, `Taylor degree: ${degree}`, canvas.width / 2, 50, 14, COLORS.orange); - } - - function factorial(n) { - if (n <= 1) return 1; - return n * factorial(n - 1); - } - - const slider = document.getElementById('slider68degree'); - const label = document.getElementById('label68degree'); - const select = document.getElementById('select68func'); - - if (slider) { - slider.addEventListener('input', (e) => { - degree = parseInt(e.target.value); - if (label) label.textContent = degree; - draw(); - }); - } - - if (select) { - select.addEventListener('change', (e) => { - func = e.target.value; - draw(); - }); - } - - draw(); + container.appendChild(label); + } + }); + } + + categoryRadios.forEach(radio => radio.addEventListener('change', updateCheckboxes)); + updateCheckboxes(); } -// ===== INTERACTIVE ELEMENTS ===== -function initInteractiveElements() { - // Add any additional interactive elements here - // Such as tooltips, modals, etc. +function updateSelection() { + const checkboxes = document.querySelectorAll('#algorithm-checkboxes input[type="checkbox"]:checked'); + comparisonState.selectedAlgorithms = Array.from(checkboxes).map(cb => cb.value); + + const count = comparisonState.selectedAlgorithms.length; + const countEl = document.getElementById('selection-count'); + const compareBtn = document.getElementById('compare-btn'); + + if (countEl) { + countEl.textContent = `Selected: ${count} algorithm${count !== 1 ? 's' : ''}`; + countEl.style.color = count >= 2 && count <= 5 ? 'var(--color-success)' : 'var(--color-error)'; + } + + if (compareBtn) { + compareBtn.disabled = count < 2 || count > 5; + } } -// ===== HELPER FUNCTIONS ===== -function generateRandomData(count, min, max) { - return Array.from({ length: count }, () => - Math.floor(Math.random() * (max - min + 1)) + min - ); +function initComparisonListeners() { + const compareBtn = document.getElementById('compare-btn'); + if (compareBtn) { + compareBtn.addEventListener('click', showComparison); + } + + const viewBtns = document.querySelectorAll('.view-btn'); + viewBtns.forEach(btn => { + btn.addEventListener('click', () => { + viewBtns.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + + const view = btn.dataset.view; + document.querySelectorAll('.comparison-view').forEach(v => v.style.display = 'none'); + const targetView = document.getElementById(`view-${view}`); + if (targetView) targetView.style.display = 'block'; + }); + }); } -function formatNumber(num, decimals = 2) { - return Number(num).toFixed(decimals); +function showComparison() { + const resultsDiv = document.getElementById('comparison-results'); + if (!resultsDiv) return; + + resultsDiv.style.display = 'block'; + resultsDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + + renderComparisonTable(); + renderRadarChart(); + renderHeatmap(); + renderUseCaseMatrix(); + renderDetailedCards(); } -// ===== ANIMATION LOOP ===== -function startAnimation(canvasId, animationFunction) { - if (animationFrames[canvasId]) { - cancelAnimationFrame(animationFrames[canvasId]); - } - - function animate() { - animationFunction(); - animationFrames[canvasId] = requestAnimationFrame(animate); - } - - animate(); -} - -function stopAnimation(canvasId) { - if (animationFrames[canvasId]) { - cancelAnimationFrame(animationFrames[canvasId]); - delete animationFrames[canvasId]; - } +function renderComparisonTable() { + const table = document.getElementById('comparison-table'); + if (!table) return; + + const metrics = [ + { key: 'speed', label: 'Speed', format: (v) => '⭐'.repeat(v) }, + { key: 'accuracy', label: 'Accuracy', format: (v) => '⭐'.repeat(v) }, + { key: 'dataRequired', label: 'Data Required', format: (v) => ['Small', 'Small', 'Medium', 'Large', 'Very Large'][v] }, + { key: 'interpretability', label: 'Interpretability', format: (v) => '⭐'.repeat(v) }, + { key: 'featureScaling', label: 'Feature Scaling' }, + { key: 'nonLinear', label: 'Handles Non-linear' }, + { key: 'trainingTime', label: 'Training Time' }, + { key: 'memoryUsage', label: 'Memory Usage' }, + { key: 'bestFor', label: 'Best For' } + ]; + + let html = 'Metric'; + comparisonState.selectedAlgorithms.forEach(name => { + html += `${name}`; + }); + html += ''; + + metrics.forEach(metric => { + html += `${metric.label}`; + comparisonState.selectedAlgorithms.forEach(name => { + const algo = comparisonState.algorithmData[name]; + const value = algo[metric.key]; + const display = metric.format ? metric.format(value) : value; + html += `${display}`; + }); + html += ''; + }); + + html += ''; + table.innerHTML = html; } -// ===== CONSOLE LOG ===== -// ===== DATA SCIENCE VISUALIZATIONS ===== +let radarComparisonChart = null; -function initSimpleRegressionCanvas() { - const canvas = document.getElementById('canvas-70'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let showLine = false; - - // Sample data - const data = [ - {x: 1, y: 2.1}, {x: 2, y: 4.2}, {x: 3, y: 5.8}, {x: 4, y: 8.1}, - {x: 5, y: 10.3}, {x: 6, y: 12.1}, {x: 7, y: 13.9}, {x: 8, y: 16.2}, - {x: 9, y: 18.1}, {x: 10, y: 20.0} - ]; - - function calculateRegression() { - const n = data.length; - const sumX = data.reduce((s, p) => s + p.x, 0); - const sumY = data.reduce((s, p) => s + p.y, 0); - const sumXY = data.reduce((s, p) => s + p.x * p.y, 0); - const sumX2 = data.reduce((s, p) => s + p.x * p.x, 0); - - const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); - const intercept = (sumY - slope * sumX) / n; - - return { slope, intercept }; - } - - function draw() { - clearCanvas(ctx, canvas); - - const padding = 60; - const width = canvas.width - 2 * padding; - const height = canvas.height - 2 * padding; - - const maxX = 11; - const maxY = 22; - - // Draw axes - drawLine(ctx, padding, canvas.height - padding, canvas.width - padding, canvas.height - padding, COLORS.text, 2); - drawLine(ctx, padding, padding, padding, canvas.height - padding, COLORS.text, 2); - - // Draw grid - for (let i = 0; i <= 10; i++) { - const x = padding + (i / maxX) * width; - const y = canvas.height - padding - (i * 2 / maxY) * height; - drawLine(ctx, x, canvas.height - padding, x, canvas.height - padding + 5, COLORS.textSecondary, 1); - drawText(ctx, i.toString(), x, canvas.height - padding + 20, 10, COLORS.textSecondary); - - if (i * 2 <= maxY) { - drawLine(ctx, padding - 5, y, padding, y, COLORS.textSecondary, 1); - drawText(ctx, (i * 2).toString(), padding - 15, y + 4, 10, COLORS.textSecondary, 'right'); - } +function renderRadarChart() { + const canvas = document.getElementById('radar-comparison-canvas'); + if (!canvas) return; + + if (radarComparisonChart) { + radarComparisonChart.destroy(); + radarComparisonChart = null; + } + + const ctx = canvas.getContext('2d'); + canvas.width = canvas.offsetWidth; + canvas.height = 500; + + const colors = ['#6aa9ff', '#7ef0d4', '#ff8c6a', '#ffeb3b', '#ffb490']; + + const datasets = comparisonState.selectedAlgorithms.map((name, i) => { + const algo = comparisonState.algorithmData[name]; + return { + label: name, + data: [algo.speed, algo.accuracy, 5 - algo.dataRequired, algo.interpretability, algo.scalability], + borderColor: colors[i], + backgroundColor: colors[i] + '33', + borderWidth: 2, + pointRadius: 4 + }; + }); + + radarComparisonChart = safeCreateChart(ctx, { + type: 'radar', + data: { + labels: ['Speed', 'Accuracy', 'Data Efficiency', 'Interpretability', 'Scalability'], + datasets: datasets + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top', + labels: { color: '#a9b4c2', padding: 15 } } - - // Draw labels - drawText(ctx, 'X', canvas.width - padding + 20, canvas.height - padding + 4, 12, COLORS.cyan); - drawText(ctx, 'Y', padding - 4, padding - 20, 12, COLORS.cyan); - - // Draw data points - data.forEach(point => { - const x = padding + (point.x / maxX) * width; - const y = canvas.height - padding - (point.y / maxY) * height; - drawCircle(ctx, x, y, 6, COLORS.cyan); - }); - - // Draw regression line - if (showLine) { - const { slope, intercept } = calculateRegression(); - const x1 = 0; - const y1 = intercept; - const x2 = maxX; - const y2 = slope * x2 + intercept; - - const px1 = padding + (x1 / maxX) * width; - const py1 = canvas.height - padding - (y1 / maxY) * height; - const px2 = padding + (x2 / maxX) * width; - const py2 = canvas.height - padding - (y2 / maxY) * height; - - drawLine(ctx, px1, py1, px2, py2, COLORS.orange, 3); - - // Calculate R² - const meanY = data.reduce((s, p) => s + p.y, 0) / data.length; - let ssTot = 0, ssRes = 0; - data.forEach(point => { - const predicted = slope * point.x + intercept; - ssRes += Math.pow(point.y - predicted, 2); - ssTot += Math.pow(point.y - meanY, 2); - }); - const r2 = 1 - (ssRes / ssTot); - - drawText(ctx, `y = ${intercept.toFixed(2)} + ${slope.toFixed(2)}x`, canvas.width / 2, 30, 14, COLORS.orange); - drawText(ctx, `R² = ${r2.toFixed(4)}`, canvas.width / 2, 50, 14, COLORS.green); - } else { - drawText(ctx, 'Click "Fit Regression Line" to see the best fit', canvas.width / 2, 30, 14, COLORS.text); + }, + scales: { + r: { + beginAtZero: true, + max: 5, + ticks: { color: '#a9b4c2', backdropColor: 'transparent' }, + grid: { color: '#2a3544' }, + pointLabels: { color: '#e8eef6', font: { size: 12 } } } + } } - - const fitBtn = document.getElementById('btn70fit'); - const resetBtn = document.getElementById('btn70reset'); - - if (fitBtn) { - fitBtn.addEventListener('click', () => { - showLine = true; - draw(); - }); - } - - if (resetBtn) { - resetBtn.addEventListener('click', () => { - showLine = false; - draw(); - }); - } - - draw(); + }, 'Radar Comparison Chart'); } -function initLogisticRegressionCanvas() { - const canvas = document.getElementById('canvas-72'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let threshold = 0.5; - - function sigmoid(z) { - return 1 / (1 + Math.exp(-z)); - } - - function draw() { - clearCanvas(ctx, canvas); - - const padding = 60; - const width = canvas.width - 2 * padding; - const height = canvas.height - 2 * padding; - - // Draw axes - const centerY = canvas.height / 2; - drawLine(ctx, padding, centerY, canvas.width - padding, centerY, COLORS.text, 2); - drawLine(ctx, canvas.width / 2, padding, canvas.width / 2, canvas.height - padding, COLORS.text, 2); - - // Draw sigmoid curve - ctx.beginPath(); - for (let x = -6; x <= 6; x += 0.1) { - const px = canvas.width / 2 + (x / 6) * (width / 2); - const y = sigmoid(x); - const py = canvas.height - padding - y * height; - if (x === -6) { - ctx.moveTo(px, py); - } else { - ctx.lineTo(px, py); - } - } - ctx.strokeStyle = COLORS.cyan; - ctx.lineWidth = 3; - ctx.stroke(); - - // Draw threshold line - const thresholdY = canvas.height - padding - threshold * height; - drawLine(ctx, padding, thresholdY, canvas.width - padding, thresholdY, COLORS.orange, 2); - drawText(ctx, `Threshold = ${threshold.toFixed(2)}`, canvas.width - 100, thresholdY - 10, 12, COLORS.orange); - - // Draw labels - drawText(ctx, 'P(y=1) = σ(z) = 1/(1+e^(-z))', canvas.width / 2, 30, 14, COLORS.cyan); - drawText(ctx, 'Class 1 (if P ≥ threshold)', canvas.width - 150, thresholdY - 30, 12, COLORS.green); - drawText(ctx, 'Class 0 (if P < threshold)', canvas.width - 150, thresholdY + 30, 12, COLORS.textSecondary); - - // Draw axis labels - drawText(ctx, '0', canvas.width / 2 + 5, centerY + 20, 10, COLORS.text, 'left'); - drawText(ctx, '1', padding - 20, padding + 10, 10, COLORS.text); - } - - const slider = document.getElementById('slider72'); - const label = document.getElementById('label72'); - - if (slider) { - slider.addEventListener('input', (e) => { - threshold = parseFloat(e.target.value); - if (label) label.textContent = threshold.toFixed(2); - draw(); - }); - } - - draw(); +function renderHeatmap() { + const container = document.getElementById('view-heatmap'); + if (!container) return; + + // Remove canvas, use HTML table instead for 100% browser compatibility + const metrics = ['Speed', 'Accuracy', 'Data Efficiency', 'Interpretability', 'Scalability']; + const algos = comparisonState.selectedAlgorithms; + + // Helper function to get color based on value + function getHeatmapColor(value) { + const intensity = value / 5; + const r = Math.floor(255 - 149 * intensity); + const g = Math.floor(140 + 100 * intensity); + const b = Math.floor(106 + 106 * intensity); + return `rgb(${r}, ${g}, ${b})`; + } + + // Build HTML table heatmap + let html = '

Performance Heatmap (Higher is Better)

'; + html += '
'; + html += ''; + + // Header row + html += ''; + html += ''; + metrics.forEach(metric => { + html += ``; + }); + html += ''; + + // Data rows + html += ''; + algos.forEach((name, i) => { + const algo = comparisonState.algorithmData[name]; + const values = [algo.speed, algo.accuracy, 5 - algo.dataRequired, algo.interpretability, algo.scalability]; + + html += ``; + html += ``; + + values.forEach((value, j) => { + const color = getHeatmapColor(value); + const stars = '⭐'.repeat(Math.round(value)); + html += ``; + }); + html += ''; + }); + html += ''; + html += '
Algorithm${metric}
${name}`; + html += `
${value.toFixed(0)}
`; + html += `
${stars}
`; + html += `
'; + html += '
'; + + // Legend + html += '
'; + html += 'Legend: '; + html += '🔴 Low (1-2) '; + html += '🟡 Medium (3) '; + html += '🟢 High (4-5)'; + html += '
'; + + // Find the canvas and replace with our HTML + const oldCanvas = container.querySelector('#heatmap-canvas'); + if (oldCanvas) { + oldCanvas.parentElement.innerHTML = html; + } else { + container.innerHTML = html; + } } -function initPolynomialRegressionCanvas() { - const canvas = document.getElementById('canvas-74'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let degree = 1; - - // Generate sample data with some noise - const trueFunc = (x) => 0.5 * x * x - 3 * x + 5; - const data = []; - for (let x = 0; x <= 10; x += 0.5) { - data.push({ x, y: trueFunc(x) + (Math.random() - 0.5) * 2 }); - } - - function fitPolynomial(degree) { - // Simple polynomial fit (not production-quality) - return (x) => { - if (degree === 1) return 0.5 * x + 1; - if (degree === 2) return 0.5 * x * x - 3 * x + 5; - if (degree === 3) return 0.05 * x * x * x - 0.2 * x * x - 2 * x + 5; - return trueFunc(x); - }; - } - - function draw() { - clearCanvas(ctx, canvas); - - const padding = 60; - const width = canvas.width - 2 * padding; - const height = canvas.height - 2 * padding; - - const maxX = 10; - const maxY = 15; - - // Draw axes - drawLine(ctx, padding, canvas.height - padding, canvas.width - padding, canvas.height - padding, COLORS.text, 2); - drawLine(ctx, padding, padding, padding, canvas.height - padding, COLORS.text, 2); - - // Draw data points - data.forEach(point => { - const px = padding + (point.x / maxX) * width; - const py = canvas.height - padding - (point.y / maxY) * height; - drawCircle(ctx, px, py, 4, COLORS.cyan); - }); - - // Draw polynomial fit - const polyFunc = fitPolynomial(degree); - ctx.beginPath(); - for (let x = 0; x <= maxX; x += 0.1) { - const px = padding + (x / maxX) * width; - const y = polyFunc(x); - const py = canvas.height - padding - (y / maxY) * height; - if (x === 0) { - ctx.moveTo(px, py); - } else { - ctx.lineTo(px, py); - } - } - ctx.strokeStyle = COLORS.orange; - ctx.lineWidth = 3; - ctx.stroke(); - - drawText(ctx, `Polynomial Degree: ${degree}`, canvas.width / 2, 30, 14, COLORS.orange); - - if (degree > 5) { - drawText(ctx, 'High degree may overfit!', canvas.width / 2, 50, 12, COLORS.orange); - } - } - - const slider = document.getElementById('slider74'); - const label = document.getElementById('label74'); - - if (slider) { - slider.addEventListener('input', (e) => { - degree = parseInt(e.target.value); - if (label) label.textContent = degree; - draw(); - }); - } - - draw(); +function renderUseCaseMatrix() { + const table = document.getElementById('matrix-table'); + if (!table) return; + + const useCases = [ + { key: 'regression', label: 'Regression' }, + { key: 'classification', label: 'Classification' }, + { key: 'clustering', label: 'Clustering' }, + { key: 'speed', label: 'Speed' }, + { key: 'interpretability', label: 'Interpretability' } + ]; + + let html = 'Use Case'; + comparisonState.selectedAlgorithms.forEach(name => { + html += `${name}`; + }); + html += ''; + + useCases.forEach(useCase => { + html += `${useCase.label}`; + comparisonState.selectedAlgorithms.forEach(name => { + const algo = comparisonState.algorithmData[name]; + const value = algo.useCases[useCase.key]; + const check = '✓'.repeat(value); + html += `${check || '✗'}`; + }); + html += ''; + }); + + html += ''; + table.innerHTML = html; } -function initPCACanvas() { - const canvas = document.getElementById('canvas-77'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let showPCs = false; - - // Generate 2D data - const data = []; - for (let i = 0; i < 50; i++) { - const x = Math.random() * 4 - 2; - const y = 0.8 * x + Math.random() * 0.5; - data.push({ x, y }); - } - - function draw() { - clearCanvas(ctx, canvas); - - const centerX = canvas.width / 2; - const centerY = canvas.height / 2; - const scale = 80; - - // Draw axes - drawLine(ctx, 0, centerY, canvas.width, centerY, '#555', 1); - drawLine(ctx, centerX, 0, centerX, canvas.height, '#555', 1); +function renderDetailedCards() { + const container = document.getElementById('detailed-cards'); + if (!container) return; + + let html = '

Detailed Comparison

'; + html += '
'; + + comparisonState.selectedAlgorithms.forEach(name => { + const algo = comparisonState.algorithmData[name]; + html += ` +
+

${name}

+

${algo.category}

- // Draw data points - data.forEach(point => { - const px = centerX + point.x * scale; - const py = centerY - point.y * scale; - drawCircle(ctx, px, py, 5, COLORS.cyan); - }); +
+ ✓ Pros: +
    + ${algo.pros.map(p => `
  • ${p}
  • `).join('')} +
+
- if (showPCs) { - // Draw PC1 (main direction) - const pc1Angle = Math.atan(0.8); - const pc1Length = 150; - drawLine(ctx, - centerX - pc1Length * Math.cos(pc1Angle), - centerY + pc1Length * Math.sin(pc1Angle), - centerX + pc1Length * Math.cos(pc1Angle), - centerY - pc1Length * Math.sin(pc1Angle), - COLORS.orange, 3 - ); - drawText(ctx, 'PC1 (80% variance)', centerX + 100, centerY - 80, 14, COLORS.orange); - - // Draw PC2 (perpendicular) - const pc2Angle = pc1Angle + Math.PI / 2; - const pc2Length = 80; - drawLine(ctx, - centerX - pc2Length * Math.cos(pc2Angle), - centerY + pc2Length * Math.sin(pc2Angle), - centerX + pc2Length * Math.cos(pc2Angle), - centerY - pc2Length * Math.sin(pc2Angle), - COLORS.green, 3 - ); - drawText(ctx, 'PC2 (20% variance)', centerX - 100, centerY - 80, 14, COLORS.green); - } +
+ ✗ Cons: +
    + ${algo.cons.map(c => `
  • ${c}
  • `).join('')} +
+
- drawText(ctx, 'PCA finds directions of maximum variance', canvas.width / 2, 30, 14, COLORS.cyan); - } - - const projectBtn = document.getElementById('btn77project'); - const resetBtn = document.getElementById('btn77reset'); - - if (projectBtn) { - projectBtn.addEventListener('click', () => { - showPCs = true; - draw(); - }); - } - - if (resetBtn) { - resetBtn.addEventListener('click', () => { - showPCs = false; - draw(); - }); - } - - draw(); +
+ ⚡ Best For: ${algo.bestFor} +
+
+ `; + }); + + html += '
'; + container.innerHTML = html; } -function initGradientDescentCanvas() { - const canvas = document.getElementById('canvas-80'); - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - let learningRate = 0.1; - let animating = false; - let path = []; - - // Simple quadratic function - const f = (x) => (x - 3) * (x - 3) + 2; - const df = (x) => 2 * (x - 3); - - function draw() { - clearCanvas(ctx, canvas); - - const padding = 60; - const width = canvas.width - 2 * padding; - const height = canvas.height - 2 * padding; - const xMin = 0, xMax = 6; - const yMax = 12; - - // Draw axes - drawLine(ctx, padding, canvas.height - padding, canvas.width - padding, canvas.height - padding, COLORS.text, 2); - drawLine(ctx, padding, padding, padding, canvas.height - padding, COLORS.text, 2); - - // Draw function - ctx.beginPath(); - for (let x = xMin; x <= xMax; x += 0.05) { - const px = padding + ((x - xMin) / (xMax - xMin)) * width; - const y = f(x); - const py = canvas.height - padding - (y / yMax) * height; - if (x === xMin) { - ctx.moveTo(px, py); - } else { - ctx.lineTo(px, py); - } +function initQuiz() { + const questions = document.querySelectorAll('.quiz-question'); + const resultDiv = document.getElementById('quiz-result'); + + questions.forEach((q, idx) => { + const radios = q.querySelectorAll('input[type="radio"]'); + radios.forEach(radio => { + radio.addEventListener('change', () => { + if (idx < questions.length - 1) { + questions[idx + 1].style.display = 'block'; } - ctx.strokeStyle = COLORS.cyan; - ctx.lineWidth = 2; - ctx.stroke(); - // Draw path - if (path.length > 0) { - ctx.beginPath(); - path.forEach((point, i) => { - const px = padding + ((point.x - xMin) / (xMax - xMin)) * width; - const py = canvas.height - padding - (point.y / yMax) * height; - - if (i === 0) { - ctx.moveTo(px, py); - } else { - ctx.lineTo(px, py); - } - drawCircle(ctx, px, py, 5, COLORS.orange); - }); - ctx.strokeStyle = COLORS.orange; - ctx.lineWidth = 2; - ctx.stroke(); + if (idx === questions.length - 1) { + showQuizResult(); } - - drawText(ctx, 'Gradient Descent: Following negative gradient to minimum', canvas.width / 2, 30, 14, COLORS.cyan); - drawText(ctx, `Learning Rate: ${learningRate.toFixed(2)}`, canvas.width / 2, 50, 12, COLORS.text); - } - - function startDescent() { - if (animating) return; - animating = true; - path = [{ x: 0.5, y: f(0.5) }]; - - const interval = setInterval(() => { - const current = path[path.length - 1]; - const grad = df(current.x); - const nextX = current.x - learningRate * grad; - - if (Math.abs(grad) < 0.01 || path.length > 50) { - clearInterval(interval); - animating = false; - return; - } - - path.push({ x: nextX, y: f(nextX) }); - draw(); - }, 200); - } - - const slider = document.getElementById('slider80'); - const label = document.getElementById('label80'); - const startBtn = document.getElementById('btn80start'); - const resetBtn = document.getElementById('btn80reset'); - - if (slider) { - slider.addEventListener('input', (e) => { - learningRate = parseFloat(e.target.value); - if (label) label.textContent = learningRate.toFixed(2); - }); - } - - if (startBtn) { - startBtn.addEventListener('click', startDescent); + }); + }); + }); + + function showQuizResult() { + const q1 = document.querySelector('input[name="q1"]:checked')?.value; + const q2 = document.querySelector('input[name="q2"]:checked')?.value; + const q3 = document.querySelector('input[name="q3"]:checked')?.value; + const q4 = document.querySelector('input[name="q4"]:checked')?.value; + + let recommendation = ''; + let alternatives = []; + + if (q1 === 'no') { + recommendation = 'K-means'; + alternatives = ['PCA', 'DBSCAN']; + } else if (q2 === 'numbers') { + if (q3 === 'little') { + recommendation = 'Linear Regression'; + alternatives = ['Decision Trees']; + } else { + recommendation = 'Random Forest'; + alternatives = ['XGBoost', 'Linear Regression']; + } + } else if (q2 === 'categories') { + if (q4 === 'very') { + recommendation = 'Decision Trees'; + alternatives = ['Logistic Regression', 'Naive Bayes']; + } else if (q3 === 'little') { + recommendation = 'Naive Bayes'; + alternatives = ['Logistic Regression']; + } else { + recommendation = 'Random Forest'; + alternatives = ['SVM', 'XGBoost']; + } + } else { + recommendation = 'K-means'; + alternatives = ['PCA']; } - if (resetBtn) { - resetBtn.addEventListener('click', () => { - path = []; - animating = false; - draw(); - }); + if (resultDiv) { + resultDiv.style.display = 'block'; + resultDiv.innerHTML = ` +

🎯 Recommendation: ${recommendation}

+

Based on your answers, ${recommendation} is the best fit for your use case.

+

Other good choices:

+ + `; + resultDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } - - draw(); + } } -function initLossLandscapeCanvas() { - const canvas = document.getElementById('canvas-85'); - if (!canvas) return; +function drawDecisionFlowchart() { + const canvas = document.getElementById('decision-flowchart'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width = canvas.offsetWidth; + const height = canvas.height = 500; + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = '#1a2332'; + ctx.fillRect(0, 0, width, height); + + const nodes = [ + { x: width/2, y: 50, text: 'Start:\nWhat problem?', w: 140, h: 60, color: '#7ef0d4', type: 'start' }, + { x: width/4, y: 160, text: 'Classification', w: 120, h: 50, color: '#6aa9ff', type: 'decision' }, + { x: width/2, y: 160, text: 'Regression', w: 120, h: 50, color: '#6aa9ff', type: 'decision' }, + { x: 3*width/4, y: 160, text: 'Clustering', w: 120, h: 50, color: '#6aa9ff', type: 'decision' }, + { x: width/8, y: 270, text: 'Linear?', w: 100, h: 50, color: '#ffb490', type: 'question' }, + { x: 3*width/8, y: 270, text: 'Fast?', w: 100, h: 50, color: '#ffb490', type: 'question' }, + { x: width/2, y: 270, text: 'Linear?', w: 100, h: 50, color: '#ffb490', type: 'question' }, + { x: 3*width/4, y: 270, text: 'Known K?', w: 100, h: 50, color: '#ffb490', type: 'question' }, + { x: width/16, y: 380, text: 'Logistic\nRegression', w: 90, h: 50, color: '#7ef0d4', type: 'result' }, + { x: 3*width/16, y: 380, text: 'SVM', w: 90, h: 50, color: '#7ef0d4', type: 'result' }, + { x: 5*width/16, y: 380, text: 'Naive\nBayes', w: 90, h: 50, color: '#7ef0d4', type: 'result' }, + { x: 7*width/16, y: 380, text: 'Random\nForest', w: 90, h: 50, color: '#7ef0d4', type: 'result' }, + { x: 9*width/16, y: 380, text: 'Linear\nRegression', w: 90, h: 50, color: '#7ef0d4', type: 'result' }, + { x: 11*width/16, y: 380, text: 'XGBoost', w: 90, h: 50, color: '#7ef0d4', type: 'result' }, + { x: 13*width/16, y: 380, text: 'K-means', w: 90, h: 50, color: '#7ef0d4', type: 'result' }, + { x: 15*width/16, y: 380, text: 'DBSCAN', w: 90, h: 50, color: '#7ef0d4', type: 'result' } + ]; + + const edges = [ + { from: 0, to: 1 }, { from: 0, to: 2 }, { from: 0, to: 3 }, + { from: 1, to: 4 }, { from: 1, to: 5 }, + { from: 2, to: 6 }, + { from: 3, to: 7 }, + { from: 4, to: 8, label: 'Yes' }, { from: 4, to: 9, label: 'No' }, + { from: 5, to: 10, label: 'Yes' }, { from: 5, to: 11, label: 'No' }, + { from: 6, to: 12, label: 'Yes' }, { from: 6, to: 13, label: 'No' }, + { from: 7, to: 14, label: 'Yes' }, { from: 7, to: 15, label: 'No' } + ]; + + // Draw edges + ctx.strokeStyle = '#6aa9ff'; + ctx.lineWidth = 2; + edges.forEach(edge => { + const from = nodes[edge.from]; + const to = nodes[edge.to]; - const ctx = canvas.getContext('2d'); - let lossType = 'mse'; - - function draw() { - clearCanvas(ctx, canvas); - - const padding = 60; - const width = canvas.width - 2 * padding; - const height = canvas.height - 2 * padding; - - // True value - const trueVal = 5; - - // Draw axes - drawLine(ctx, padding, canvas.height - padding, canvas.width - padding, canvas.height - padding, COLORS.text, 2); - drawLine(ctx, padding, padding, padding, canvas.height - padding, COLORS.text, 2); - - // Draw loss function - ctx.beginPath(); - for (let pred = 0; pred <= 10; pred += 0.1) { - let loss; - if (lossType === 'mse') { - loss = Math.pow(trueVal - pred, 2); - } else if (lossType === 'mae') { - loss = Math.abs(trueVal - pred); - } else { - // Simplified cross-entropy - loss = -Math.log(Math.max(0.01, 1 - Math.abs(trueVal - pred) / 10)); - } - - const px = padding + (pred / 10) * width; - const py = canvas.height - padding - (loss / 30) * height; - - if (pred === 0) { - ctx.moveTo(px, py); - } else { - ctx.lineTo(px, py); - } - } - ctx.strokeStyle = COLORS.cyan; - ctx.lineWidth = 3; - ctx.stroke(); - - // Draw minimum - const minX = padding + (trueVal / 10) * width; - drawLine(ctx, minX, canvas.height - padding, minX, padding, COLORS.orange, 2); - drawText(ctx, 'Minimum', minX + 10, padding + 20, 12, COLORS.orange); - - const lossNames = { - 'mse': 'Mean Squared Error: (y - ŷ)²', - 'mae': 'Mean Absolute Error: |y - ŷ|', - 'cross': 'Cross-Entropy Loss' - }; - - drawText(ctx, lossNames[lossType], canvas.width / 2, 30, 14, COLORS.cyan); - drawText(ctx, 'Predicted Value →', canvas.width - 100, canvas.height - 30, 12, COLORS.text); - drawText(ctx, 'Loss ↑', padding - 40, padding, 12, COLORS.text); - } + ctx.beginPath(); + ctx.moveTo(from.x, from.y + from.h/2); + ctx.lineTo(to.x, to.y - to.h/2); + ctx.stroke(); - const select = document.getElementById('select85'); - if (select) { - select.addEventListener('change', (e) => { - lossType = e.target.value; - draw(); - }); + if (edge.label) { + ctx.fillStyle = '#7ef0d4'; + ctx.font = '10px sans-serif'; + ctx.textAlign = 'center'; + const midX = (from.x + to.x) / 2; + const midY = (from.y + to.y) / 2; + ctx.fillText(edge.label, midX + 12, midY); } - - draw(); + }); + + // Draw nodes + nodes.forEach(node => { + const x = node.x - node.w/2; + const y = node.y - node.h/2; + + ctx.fillStyle = node.color + '33'; + ctx.fillRect(x, y, node.w, node.h); + ctx.strokeStyle = node.color; + ctx.lineWidth = 2; + ctx.strokeRect(x, y, node.w, node.h); + + ctx.fillStyle = '#e8eef6'; + ctx.font = node.type === 'result' ? 'bold 11px sans-serif' : '11px sans-serif'; + ctx.textAlign = 'center'; + const lines = node.text.split('\n'); + lines.forEach((line, i) => { + ctx.fillText(line, node.x, node.y - (lines.length - 1) * 6 + i * 12); + }); + }); + + // Title + ctx.fillStyle = '#7ef0d4'; + ctx.font = 'bold 16px sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText('Algorithm Selection Flowchart', width/2, 25); } -console.log('%c📊 Ultimate Learning Platform Loaded', 'color: #64ffda; font-size: 16px; font-weight: bold;'); -console.log('%cReady to explore 125+ comprehensive topics across 5 subjects!', 'color: #4a90e2; font-size: 14px;'); -console.log('%c✓ Statistics (41) ✓ Linear Algebra (16) ✓ Calculus (12) ✓ Data Science (16) ✓ Machine Learning (40+)', 'color: #51cf66; font-size: 12px;'); \ No newline at end of file +// Handle window resize +let resizeTimer; +window.addEventListener('resize', () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { + drawLinearRegression(); + drawGradientDescent(); + drawSigmoid(); + drawLogisticClassification(); + drawKNN(); + drawConfusionMatrix(); + drawROC(); + drawR2(); + drawRegularization(); + drawBiasVariance(); + drawComplexityCurve(); + drawCrossValidation(); + drawScaling(); + drawPipeline(); + drawLossComparison(); + drawLossCurves(); + drawSVMBasic(); + drawSVMMargin(); + drawSVMCParameter(); + drawSVMTraining(); + drawSVMKernel(); + // New topics + if (document.getElementById('elbow-canvas')) drawElbowCurve(); + if (document.getElementById('cv-k-canvas')) drawCVKHeatmap(); + if (document.getElementById('gridsearch-heatmap')) drawGridSearchHeatmap(); + if (document.getElementById('param-surface')) drawParamSurface(); + if (document.getElementById('bayes-theorem-viz')) drawBayesTheorem(); + if (document.getElementById('spam-classification')) drawSpamClassification(); + if (document.getElementById('decision-tree-viz')) drawDecisionTree(); + if (document.getElementById('entropy-viz')) drawEntropyViz(); + if (document.getElementById('split-comparison')) drawSplitComparison(); + if (document.getElementById('tree-boundary')) drawTreeBoundary(); + if (document.getElementById('bagging-viz')) drawBaggingViz(); + if (document.getElementById('boosting-viz')) drawBoostingViz(); + if (document.getElementById('random-forest-viz')) drawRandomForestViz(); + if (document.getElementById('categorical-nb-canvas')) drawCategoricalNB(); + if (document.getElementById('gaussian-nb-canvas')) drawGaussianNB(); + if (document.getElementById('kmeans-viz-canvas')) drawKMeansVisualization(); + if (document.getElementById('kmeans-elbow-canvas')) drawKMeansElbow(); + if (document.getElementById('decision-flowchart')) drawDecisionFlowchart(); + }, 250); +});