Spaces:
Running
Running
| // ===== CONFIGURATION & CONSTANTS ===== | |
| const COLORS = { | |
| primary: '#4a90e2', | |
| cyan: '#64ffda', | |
| orange: '#ff6b6b', | |
| green: '#51cf66', | |
| background: '#0f3460', | |
| text: '#e1e1e1', | |
| textSecondary: '#a0a0a0' | |
| }; | |
| const chartColors = ['#1FB8CD', '#FFC185', '#B4413C', '#ECEBD5', '#5D878F', '#DB4545', '#D2BA4C', '#964325', '#944454', '#13343B']; | |
| // ===== STATE MANAGEMENT ===== | |
| let currentTopic = 1; | |
| let currentSubject = 'statistics'; | |
| let animationFrames = {}; | |
| // ===== INITIALIZATION ===== | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Core initialization | |
| initNavigation(); | |
| initSubjectTabs(); | |
| initInteractiveElements(); | |
| setupScrollObserver(); | |
| initializeAllVisualizations(); | |
| // Enhanced features | |
| initSearch(); | |
| initProgressTracking(); | |
| initKeyboardShortcuts(); | |
| initLazyLoading(); | |
| // Fix Show Answer buttons (Remove inline handlers to avoid conflicts) | |
| document.querySelectorAll('.show-answers-btn, .show-answer-btn').forEach(btn => { | |
| btn.removeAttribute('onclick'); | |
| }); | |
| // Show initial subject | |
| switchSubject('statistics'); | |
| // Log features | |
| console.log('%cπ Enhanced Features Active:', 'color: #64ffda; font-size: 14px; font-weight: bold;'); | |
| console.log('%c β Global Search (Ctrl/Cmd+K)', 'color: #4a90e2;'); | |
| console.log('%c β Progress Tracking (saved locally)', 'color: #4a90e2;'); | |
| console.log('%c β MathJax Formula Rendering', 'color: #4a90e2;'); | |
| console.log('%c β Keyboard Navigation (Alt+β/β)', 'color: #4a90e2;'); | |
| }); | |
| // ===== SUBJECT SWITCHING ===== | |
| function initSubjectTabs() { | |
| const tabs = document.querySelectorAll('.subject-tab'); | |
| tabs.forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| const subject = tab.dataset.subject; | |
| switchSubject(subject); | |
| }); | |
| }); | |
| } | |
| 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'); | |
| } | |
| }); | |
| // 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'; | |
| } | |
| }); | |
| // Show/hide topic sections | |
| document.querySelectorAll('.topic-section').forEach(section => { | |
| const sectionSubject = section.dataset.subject || 'statistics'; | |
| section.style.display = sectionSubject === subject ? 'block' : 'none'; | |
| }); | |
| // 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); | |
| } | |
| } | |
| } | |
| // ===== 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'); | |
| }); | |
| } | |
| // Topic link navigation | |
| const topicLinks = document.querySelectorAll('.topic-link'); | |
| topicLinks.forEach(link => { | |
| link.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const topicId = link.getAttribute('data-topic'); | |
| // Handle both 'topic-X' and 'ml-topic-X' formats | |
| let target = document.getElementById(topicId); | |
| if (!target && !topicId.includes('-')) { | |
| 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'); | |
| } | |
| } | |
| }); | |
| }); | |
| } | |
| function updateActiveLink(topicId) { | |
| document.querySelectorAll('.topic-link').forEach(link => { | |
| link.classList.remove('active'); | |
| }); | |
| const activeLink = document.querySelector(`[data-topic="${topicId}"]`); | |
| if (activeLink) { | |
| activeLink.classList.add('active'); | |
| } | |
| currentTopic = parseInt(topicId); | |
| } | |
| // ===== SCROLL OBSERVER ===== | |
| function setupScrollObserver() { | |
| const options = { | |
| root: null, | |
| rootMargin: '-100px', | |
| threshold: 0.3 | |
| }; | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| // Handle both 'topic-X' and 'ml-topic-X' formats | |
| const fullId = entry.target.id; | |
| updateActiveLink(fullId); | |
| } | |
| }); | |
| }, options); | |
| document.querySelectorAll('.topic-section').forEach(section => { | |
| observer.observe(section); | |
| }); | |
| } | |
| // ===== CANVAS UTILITIES ===== | |
| function clearCanvas(ctx, canvas) { | |
| ctx.fillStyle = COLORS.background; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| } | |
| 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 drawCircle(ctx, x, y, radius, color, filled = true) { | |
| 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(); | |
| } | |
| } | |
| function drawLine(ctx, x1, y1, x2, y2, color = COLORS.text, width = 1) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x1, y1); | |
| ctx.lineTo(x2, y2); | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = width; | |
| ctx.stroke(); | |
| } | |
| function drawRect(ctx, x, y, width, height, color, filled = true) { | |
| if (filled) { | |
| ctx.fillStyle = color; | |
| ctx.fillRect(x, y, width, height); | |
| } else { | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(x, y, width, height); | |
| } | |
| } | |
| // ===== STATISTICAL CALCULATIONS ===== | |
| function calculateMean(data) { | |
| return data.reduce((sum, val) => sum + val, 0) / data.length; | |
| } | |
| 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 calculateMode(data) { | |
| const frequency = {}; | |
| let maxFreq = 0; | |
| data.forEach(val => { | |
| frequency[val] = (frequency[val] || 0) + 1; | |
| maxFreq = Math.max(maxFreq, frequency[val]); | |
| }); | |
| if (maxFreq === 1) return 'None'; | |
| const modes = Object.keys(frequency).filter(key => frequency[key] === maxFreq); | |
| return modes.join(', '); | |
| } | |
| 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 calculateStdDev(data, isSample = true) { | |
| return Math.sqrt(calculateVariance(data, isSample)); | |
| } | |
| 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); | |
| return { q1, q2, q3 }; | |
| } | |
| 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 }; | |
| } | |
| function detectOutliers(data) { | |
| const { lowerFence, upperFence } = calculateIQR(data); | |
| return data.filter(val => val < lowerFence || val > upperFence); | |
| } | |
| function calculateCovariance(x, y) { | |
| const meanX = calculateMean(x); | |
| const meanY = calculateMean(y); | |
| let sum = 0; | |
| for (let i = 0; i < x.length; i++) { | |
| sum += (x[i] - meanX) * (y[i] - meanY); | |
| } | |
| return sum / (x.length - 1); | |
| } | |
| function calculateCorrelation(x, y) { | |
| const cov = calculateCovariance(x, y); | |
| const stdX = calculateStdDev(x); | |
| const stdY = calculateStdDev(y); | |
| return cov / (stdX * stdY); | |
| } | |
| // ===== VISUALIZATION FUNCTIONS ===== | |
| // 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; | |
| } | |
| }); | |
| } | |
| draw(); | |
| } | |
| // 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(); | |
| }); | |
| } | |
| if (input) { | |
| input.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| data = parseInput(input.value); | |
| draw(); | |
| } | |
| }); | |
| } | |
| draw(); | |
| } | |
| // ===== INITIALIZE ALL VISUALIZATIONS ===== | |
| function initializeAllVisualizations() { | |
| // Statistics visualizations | |
| initPopulationSampleViz(); | |
| initCentralTendencyViz(); | |
| // Linear Algebra visualizations | |
| initVectorCanvas(); | |
| initSpanCanvas(); | |
| initTransformationGrid(); | |
| initEigenvectorCanvas(); // This now calls the updated version | |
| // Advanced Stats (New) | |
| initSkewnessVisualization(); | |
| initCovarianceVisualization(); | |
| initPDFCDFVisualization(); | |
| initNormalRuleVisualization(); | |
| initCorrelationVisualization(); | |
| // Calculus visualizations | |
| initCircleAreaCanvas(); | |
| initDerivativeCanvas(); | |
| initRiemannSumCanvas(); | |
| initTaylorSeriesCanvas(); | |
| // Data Science visualizations | |
| initSimpleRegressionCanvas(); | |
| initLogisticRegressionCanvas(); | |
| initPolynomialRegressionCanvas(); | |
| initPCACanvas(); | |
| initGradientDescentCanvas(); | |
| initLossLandscapeCanvas(); | |
| // Machine Learning (New) | |
| initDecisionTreeVisualization(); | |
| // Machine Learning visualizations | |
| initMLLinearRegressionCanvas(); | |
| initMLKMeansCanvas(); | |
| initMLSVMCanvas(); | |
| initMLRandomForestCanvas(); | |
| initMLGradientBoostingCanvas(); | |
| initMLNeuralNetworkCanvas(); | |
| } | |
| // ===== MACHINE LEARNING VISUALIZATIONS ===== | |
| 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'); | |
| } | |
| // 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'); | |
| } | |
| } | |
| 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(); | |
| } | |
| 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 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); | |
| } | |
| 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'); | |
| } | |
| // 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(); | |
| } | |
| // ===== LINEAR ALGEBRA VISUALIZATIONS ===== | |
| 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); | |
| } | |
| } | |
| // 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(); | |
| } | |
| 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(); | |
| } | |
| function initTransformationGrid() { | |
| const canvas = document.getElementById('canvas-44'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let matrix = [[1, 0], [0, 1]]; // Identity | |
| 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); | |
| } | |
| 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); | |
| }); | |
| } | |
| drawGrid(false); | |
| } | |
| // ===== EIGENVECTOR VISUALIZATION (UPDATED) ===== | |
| function initEigenvectorCanvas() { | |
| const canvas = document.getElementById('canvas-54'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let transformed = false; | |
| // Matrix A = [[2, 0], [0, 1]] | |
| const matrix = [[2, 0], [0, 1]]; | |
| // Test vectors | |
| const vectors = [ | |
| { x: 1, y: 1, color: '#4a90e2', label: '[1,1] Not Eigenvector', isEigen: false }, | |
| { x: 1, y: 0, color: '#51cf66', label: '[1,0] Eigenvector \u03bb=2', isEigen: true, lambda: 2 }, | |
| { x: 0, y: 1, color: '#ffd93d', label: '[0,1] Eigenvector \u03bb=1', isEigen: true, lambda: 1 } | |
| ]; | |
| function applyMatrix(v) { | |
| return { | |
| x: matrix[0][0] * v.x + matrix[0][1] * v.y, | |
| y: matrix[1][0] * v.x + matrix[1][1] * v.y | |
| }; | |
| } | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| const centerX = canvas.width / 2; | |
| const centerY = canvas.height / 2; | |
| const scale = 80; | |
| // Title | |
| drawText(ctx, transformed ? 'After: A \u00d7 v (Matrix Applied)' : 'Before: Original Vectors', centerX, 25, 16, COLORS.cyan); | |
| drawText(ctx, 'Matrix A = [[2, 0], [0, 1]] \u2014 Stretches x-axis by 2', centerX, 45, 12, COLORS.textSecondary); | |
| // Draw grid | |
| ctx.strokeStyle = 'rgba(255,255,255,0.1)'; | |
| ctx.lineWidth = 1; | |
| for (let i = -4; i <= 4; i++) { | |
| drawLine(ctx, centerX + i * scale, 60, centerX + i * scale, canvas.height - 40, 'rgba(255,255,255,0.1)', 1); | |
| drawLine(ctx, 40, centerY + i * scale, canvas.width - 40, centerY + i * scale, 'rgba(255,255,255,0.1)', 1); | |
| } | |
| // Draw axes | |
| drawLine(ctx, 40, centerY, canvas.width - 40, centerY, COLORS.text, 2); | |
| drawLine(ctx, centerX, 60, centerX, canvas.height - 40, COLORS.text, 2); | |
| // Draw vectors | |
| vectors.forEach((v, idx) => { | |
| let displayV = transformed ? applyMatrix(v) : v; | |
| const endX = centerX + displayV.x * scale; | |
| const endY = centerY - displayV.y * scale; | |
| // Draw vector arrow | |
| ctx.beginPath(); | |
| ctx.strokeStyle = v.color; | |
| ctx.lineWidth = 3; | |
| ctx.moveTo(centerX, centerY); | |
| ctx.lineTo(endX, endY); | |
| ctx.stroke(); | |
| // Arrow head | |
| const angle = Math.atan2(centerY - endY, endX - centerX); | |
| ctx.beginPath(); | |
| ctx.fillStyle = v.color; | |
| ctx.moveTo(endX, endY); | |
| ctx.lineTo(endX - 15 * Math.cos(angle - 0.3), endY + 15 * Math.sin(angle - 0.3)); | |
| ctx.lineTo(endX - 15 * Math.cos(angle + 0.3), endY + 15 * Math.sin(angle + 0.3)); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| // Label | |
| const labelX = endX + 20; | |
| const labelY = endY - 10; | |
| drawText(ctx, `[${displayV.x.toFixed(1)}, ${displayV.y.toFixed(1)}]`, labelX, labelY, 11, v.color, 'left'); | |
| }); | |
| // Legend | |
| const legendY = canvas.height - 25; | |
| vectors.forEach((v, idx) => { | |
| const x = 80 + idx * 220; | |
| drawCircle(ctx, x, legendY, 6, v.color); | |
| drawText(ctx, v.label, x + 15, legendY + 4, 10, COLORS.text, 'left'); | |
| }); | |
| } | |
| const transformBtn = document.getElementById('btn54transform'); | |
| const resetBtn = document.getElementById('btn54reset'); | |
| if (transformBtn) { | |
| transformBtn.addEventListener('click', () => { | |
| transformed = true; | |
| draw(); | |
| }); | |
| } | |
| if (resetBtn) { | |
| resetBtn.addEventListener('click', () => { | |
| transformed = false; | |
| draw(); | |
| }); | |
| } | |
| draw(); | |
| } | |
| // ===== CALCULUS VISUALIZATIONS ===== | |
| function initCircleAreaCanvas() { | |
| const canvas = document.getElementById('canvas-58'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let unwrapping = false; | |
| let progress = 0; | |
| 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); | |
| } | |
| } | |
| } | |
| 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); | |
| }); | |
| } | |
| if (resetBtn) { | |
| resetBtn.addEventListener('click', () => { | |
| unwrapping = false; | |
| progress = 0; | |
| draw(); | |
| }); | |
| } | |
| draw(); | |
| } | |
| 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 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); | |
| } | |
| const slider = document.getElementById('slider59dx'); | |
| const label = document.getElementById('label59dx'); | |
| if (slider) { | |
| slider.addEventListener('input', (e) => { | |
| dx = parseFloat(e.target.value); | |
| if (label) label.textContent = dx.toFixed(1); | |
| draw(); | |
| }); | |
| } | |
| draw(); | |
| } | |
| function initRiemannSumCanvas() { | |
| const canvas = document.getElementById('canvas-64'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let n = 8; | |
| function f(x) { | |
| return 50 + 50 * Math.sin(x / 30); | |
| } | |
| 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); | |
| } | |
| } | |
| 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); | |
| } | |
| 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(); | |
| }); | |
| } | |
| draw(); | |
| } | |
| 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); | |
| const scale = 50; | |
| const centerX = canvas.width / 2; | |
| const centerY = canvas.height / 2; | |
| // Draw axes | |
| drawLine(ctx, 50, centerY, canvas.width - 50, centerY, '#555', 1); | |
| drawLine(ctx, centerX, 50, centerX, canvas.height - 50, '#555', 1); | |
| // 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(); | |
| // 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(); | |
| 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(); | |
| } | |
| // ===== INTERACTIVE ELEMENTS ===== | |
| function initInteractiveElements() { | |
| // Add any additional interactive elements here | |
| // Such as tooltips, modals, etc. | |
| } | |
| // ===== HELPER FUNCTIONS ===== | |
| function generateRandomData(count, min, max) { | |
| return Array.from({ length: count }, () => | |
| Math.floor(Math.random() * (max - min + 1)) + min | |
| ); | |
| } | |
| function formatNumber(num, decimals = 2) { | |
| return Number(num).toFixed(decimals); | |
| } | |
| // ===== 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]; | |
| } | |
| } | |
| // ===== CONSOLE LOG ===== | |
| // ===== ADDITIONAL ML VISUALIZATIONS ===== | |
| function initMLSVMCanvas() { | |
| const canvas = document.getElementById('canvas-ml-9'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| // Generate two-class data | |
| const class1 = Array.from({ length: 20 }, () => ({ | |
| x: Math.random() * 100 + 50, | |
| y: Math.random() * 100 + 50 | |
| })); | |
| const class2 = Array.from({ length: 20 }, () => ({ | |
| x: Math.random() * 100 + 200, | |
| y: Math.random() * 100 + 200 | |
| })); | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| const padding = 50; | |
| // Draw decision boundary (simplified) | |
| drawLine(ctx, padding, canvas.height - padding, canvas.width - padding, padding, COLORS.orange, 3); | |
| drawText(ctx, 'Decision Boundary', canvas.width / 2, 30, 14, COLORS.orange); | |
| // Draw margin lines | |
| drawLine(ctx, padding, canvas.height - padding - 30, canvas.width - padding, padding - 30, COLORS.green, 1); | |
| drawLine(ctx, padding, canvas.height - padding + 30, canvas.width - padding, padding + 30, COLORS.green, 1); | |
| drawText(ctx, 'Maximum Margin', canvas.width / 2, 50, 12, COLORS.green); | |
| // Draw data points | |
| class1.forEach(p => drawCircle(ctx, p.x, p.y, 6, COLORS.cyan)); | |
| class2.forEach(p => drawCircle(ctx, p.x, p.y, 6, COLORS.primary)); | |
| } | |
| draw(); | |
| } | |
| function initMLRandomForestCanvas() { | |
| const canvas = document.getElementById('canvas-ml-12'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| drawText(ctx, 'Random Forest: Ensemble of Decision Trees', canvas.width / 2, 50, 16, COLORS.cyan); | |
| // Draw multiple trees | |
| const treeCount = 5; | |
| const treeWidth = (canvas.width - 100) / treeCount; | |
| for (let i = 0; i < treeCount; i++) { | |
| const x = 50 + i * treeWidth + treeWidth / 2; | |
| const y = 100; | |
| // Draw simple tree structure | |
| drawLine(ctx, x, y, x - 30, y + 60, COLORS.green, 2); | |
| drawLine(ctx, x, y, x + 30, y + 60, COLORS.green, 2); | |
| drawCircle(ctx, x, y, 8, COLORS.cyan); | |
| drawCircle(ctx, x - 30, y + 60, 6, COLORS.orange); | |
| drawCircle(ctx, x + 30, y + 60, 6, COLORS.orange); | |
| drawText(ctx, `Tree ${i + 1}`, x, y + 100, 12, COLORS.text); | |
| } | |
| // Draw voting arrow | |
| drawLine(ctx, canvas.width / 2, 200, canvas.width / 2, 280, COLORS.orange, 3); | |
| drawText(ctx, 'β Majority Vote', canvas.width / 2 + 10, 250, 14, COLORS.orange, 'left'); | |
| drawRect(ctx, canvas.width / 2 - 80, 280, 160, 40, COLORS.green); | |
| drawText(ctx, 'Final Prediction', canvas.width / 2, 305, 14, '#000'); | |
| } | |
| draw(); | |
| } | |
| function initMLGradientBoostingCanvas() { | |
| const canvas = document.getElementById('canvas-ml-13'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| drawText(ctx, 'Gradient Boosting: Sequential Error Correction', canvas.width / 2, 40, 16, COLORS.cyan); | |
| const stages = 4; | |
| const stageWidth = (canvas.width - 100) / stages; | |
| for (let i = 0; i < stages; i++) { | |
| const x = 50 + i * stageWidth; | |
| const y = canvas.height / 2; | |
| // Draw tree | |
| drawRect(ctx, x, y - 40, stageWidth - 40, 80, i === 0 ? COLORS.cyan : COLORS.orange, false); | |
| drawText(ctx, `Tree ${i + 1}`, x + (stageWidth - 40) / 2, y, 12, COLORS.text); | |
| drawText(ctx, i === 0 ? 'Base' : 'Fix Errors', x + (stageWidth - 40) / 2, y + 20, 10, COLORS.textSecondary); | |
| // Draw arrow | |
| if (i < stages - 1) { | |
| drawLine(ctx, x + stageWidth - 40, y, x + stageWidth, y, COLORS.green, 2); | |
| drawText(ctx, '+', x + stageWidth - 20, y - 10, 16, COLORS.green); | |
| } | |
| } | |
| drawText(ctx, 'Each tree learns from previous mistakes', canvas.width / 2, canvas.height - 30, 12, COLORS.text); | |
| } | |
| draw(); | |
| } | |
| function initMLNeuralNetworkCanvas() { | |
| const canvas = document.getElementById('canvas-ml-14'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| drawText(ctx, 'Neural Network Architecture', canvas.width / 2, 30, 16, COLORS.cyan); | |
| const layers = [3, 5, 4, 2]; // neurons per layer | |
| const layerSpacing = (canvas.width - 100) / (layers.length - 1); | |
| // Draw connections | |
| ctx.globalAlpha = 0.3; | |
| for (let l = 0; l < layers.length - 1; l++) { | |
| const x1 = 50 + l * layerSpacing; | |
| const x2 = 50 + (l + 1) * layerSpacing; | |
| for (let i = 0; i < layers[l]; i++) { | |
| const y1 = canvas.height / 2 - (layers[l] - 1) * 15 + i * 30; | |
| for (let j = 0; j < layers[l + 1]; j++) { | |
| const y2 = canvas.height / 2 - (layers[l + 1] - 1) * 15 + j * 30; | |
| drawLine(ctx, x1, y1, x2, y2, COLORS.textSecondary, 1); | |
| } | |
| } | |
| } | |
| ctx.globalAlpha = 1; | |
| // Draw neurons | |
| layers.forEach((count, l) => { | |
| const x = 50 + l * layerSpacing; | |
| for (let i = 0; i < count; i++) { | |
| const y = canvas.height / 2 - (count - 1) * 15 + i * 30; | |
| drawCircle(ctx, x, y, 12, l === 0 ? COLORS.cyan : (l === layers.length - 1 ? COLORS.green : COLORS.orange)); | |
| } | |
| const layerNames = ['Input', 'Hidden 1', 'Hidden 2', 'Output']; | |
| drawText(ctx, layerNames[l], x, canvas.height - 30, 12, COLORS.text); | |
| }); | |
| } | |
| draw(); | |
| } | |
| // ===== DATA SCIENCE VISUALIZATIONS ===== | |
| 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'); | |
| } | |
| } | |
| // 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); | |
| } | |
| } | |
| 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(); | |
| } | |
| 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 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 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); | |
| // 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); | |
| }); | |
| 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); | |
| } | |
| 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(); | |
| } | |
| 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); | |
| } | |
| } | |
| 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(); | |
| } | |
| 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); | |
| } | |
| if (resetBtn) { | |
| resetBtn.addEventListener('click', () => { | |
| path = []; | |
| animating = false; | |
| draw(); | |
| }); | |
| } | |
| draw(); | |
| } | |
| function initLossLandscapeCanvas() { | |
| const canvas = document.getElementById('canvas-85'); | |
| if (!canvas) return; | |
| 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); | |
| } | |
| const select = document.getElementById('select85'); | |
| if (select) { | |
| select.addEventListener('change', (e) => { | |
| lossType = e.target.value; | |
| draw(); | |
| }); | |
| } | |
| draw(); | |
| } | |
| // ========== SEARCH FUNCTIONALITY ========== | |
| function initSearch() { | |
| const searchInput = document.getElementById('globalSearch'); | |
| const searchResults = document.getElementById('searchResults'); | |
| if (!searchInput || !searchResults) return; | |
| // Build search index from all topics | |
| const searchIndex = []; | |
| document.querySelectorAll('.topic-section').forEach(section => { | |
| const id = section.id; | |
| const header = section.querySelector('h2'); | |
| const subtitle = section.querySelector('.topic-subtitle'); | |
| const content = section.textContent.substring(0, 500); | |
| const subject = section.dataset.subject || 'statistics'; | |
| if (header) { | |
| searchIndex.push({ | |
| id: id, | |
| title: header.textContent, | |
| subtitle: subtitle ? subtitle.textContent : '', | |
| content: content, | |
| subject: subject | |
| }); | |
| } | |
| }); | |
| searchInput.addEventListener('input', (e) => { | |
| const query = e.target.value.toLowerCase().trim(); | |
| if (query.length < 2) { | |
| searchResults.classList.remove('active'); | |
| return; | |
| } | |
| const results = searchIndex.filter(item => | |
| item.title.toLowerCase().includes(query) || | |
| item.subtitle.toLowerCase().includes(query) || | |
| item.content.toLowerCase().includes(query) | |
| ).slice(0, 8); | |
| if (results.length === 0) { | |
| searchResults.innerHTML = '<div class="no-results">No topics found for "' + query + '"</div>'; | |
| } else { | |
| searchResults.innerHTML = results.map(result => ` | |
| <div class="search-result-item" data-topic-id="${result.id}" data-subject="${result.subject}"> | |
| <div class="search-result-title">${highlightMatch(result.title, query)}</div> | |
| <div class="search-result-subject">${result.subject.replace('-', ' ')}</div> | |
| </div> | |
| `).join(''); | |
| } | |
| searchResults.classList.add('active'); | |
| }); | |
| // Handle result clicks | |
| searchResults.addEventListener('click', (e) => { | |
| const item = e.target.closest('.search-result-item'); | |
| if (item) { | |
| const topicId = item.dataset.topicId; | |
| const subject = item.dataset.subject; | |
| // Switch to correct subject first | |
| switchSubject(subject); | |
| // Then scroll to topic | |
| setTimeout(() => { | |
| const target = document.getElementById(topicId); | |
| if (target) { | |
| target.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } | |
| }, 200); | |
| searchResults.classList.remove('active'); | |
| searchInput.value = ''; | |
| } | |
| }); | |
| // Close on outside click | |
| document.addEventListener('click', (e) => { | |
| if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) { | |
| searchResults.classList.remove('active'); | |
| } | |
| }); | |
| // Close on Escape | |
| searchInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') { | |
| searchResults.classList.remove('active'); | |
| searchInput.blur(); | |
| } | |
| }); | |
| } | |
| function highlightMatch(text, query) { | |
| const regex = new RegExp(`(${query})`, 'gi'); | |
| return text.replace(regex, '<span class="search-highlight">$1</span>'); | |
| } | |
| // ========== PROGRESS TRACKING ========== | |
| const STORAGE_KEY = 'math_mastery_progress'; | |
| function initProgressTracking() { | |
| loadProgress(); | |
| addCompletionButtons(); | |
| updateProgressDisplay(); | |
| } | |
| function loadProgress() { | |
| try { | |
| const saved = localStorage.getItem(STORAGE_KEY); | |
| if (saved) { | |
| const completed = JSON.parse(saved); | |
| completed.forEach(topicId => { | |
| markTopicComplete(topicId, false); | |
| }); | |
| } | |
| } catch (e) { | |
| console.warn('Could not load progress:', e); | |
| } | |
| } | |
| function saveProgress() { | |
| try { | |
| const completed = []; | |
| document.querySelectorAll('.topic-complete-btn.completed').forEach(btn => { | |
| completed.push(btn.dataset.topicId); | |
| }); | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(completed)); | |
| } catch (e) { | |
| console.warn('Could not save progress:', e); | |
| } | |
| } | |
| function addCompletionButtons() { | |
| document.querySelectorAll('.topic-section').forEach(section => { | |
| const topicId = section.id; | |
| const summaryCard = section.querySelector('.summary-card'); | |
| if (summaryCard && !section.querySelector('.topic-complete-btn')) { | |
| const btn = document.createElement('button'); | |
| btn.className = 'topic-complete-btn'; | |
| btn.dataset.topicId = topicId; | |
| btn.innerHTML = '<span class="check-icon"></span> Mark as Complete'; | |
| btn.addEventListener('click', () => { | |
| toggleTopicComplete(topicId); | |
| }); | |
| summaryCard.appendChild(btn); | |
| } | |
| }); | |
| } | |
| function toggleTopicComplete(topicId) { | |
| const btn = document.querySelector(`.topic-complete-btn[data-topic-id="${topicId}"]`); | |
| if (btn) { | |
| const isCompleted = btn.classList.toggle('completed'); | |
| btn.innerHTML = isCompleted | |
| ? '<span class="check-icon"></span> Completed!' | |
| : '<span class="check-icon"></span> Mark as Complete'; | |
| // Update sidebar link | |
| const sidebarLink = document.querySelector(`[data-topic="${topicId}"], [data-topic="${topicId.replace('topic-', '')}"]`); | |
| if (sidebarLink) { | |
| sidebarLink.classList.toggle('completed', isCompleted); | |
| } | |
| saveProgress(); | |
| updateProgressDisplay(); | |
| } | |
| } | |
| function markTopicComplete(topicId, save = true) { | |
| const btn = document.querySelector(`.topic-complete-btn[data-topic-id="${topicId}"]`); | |
| if (btn && !btn.classList.contains('completed')) { | |
| btn.classList.add('completed'); | |
| btn.innerHTML = '<span class="check-icon"></span> Completed!'; | |
| const sidebarLink = document.querySelector(`[data-topic="${topicId}"], [data-topic="${topicId.replace('topic-', '')}"]`); | |
| if (sidebarLink) { | |
| sidebarLink.classList.add('completed'); | |
| } | |
| if (save) { | |
| saveProgress(); | |
| updateProgressDisplay(); | |
| } | |
| } | |
| } | |
| function updateProgressDisplay() { | |
| // Progress bar removed - keeping function for compatibility | |
| // Can be used for console logging if needed | |
| } | |
| // ========== ROBUST CANVAS INITIALIZATION ========== | |
| function ensureCanvasVisible(canvasId, callback) { | |
| const canvas = document.getElementById(canvasId); | |
| if (!canvas) return; | |
| // Already initialized | |
| if (canvas.dataset.initialized === 'true') return; | |
| // Check visibility | |
| if (canvas.offsetWidth === 0) { | |
| setTimeout(() => ensureCanvasVisible(canvasId, callback), 100); | |
| return; | |
| } | |
| // Mark and execute | |
| canvas.dataset.initialized = 'true'; | |
| callback(); | |
| } | |
| // ========== LAZY LOADING FOR VISUALIZATIONS ========== | |
| function initLazyLoading() { | |
| const observerOptions = { | |
| root: null, | |
| rootMargin: '100px', | |
| threshold: 0.1 | |
| }; | |
| const lazyObserver = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| const section = entry.target; | |
| const canvases = section.querySelectorAll('canvas:not([data-initialized="true"])'); | |
| canvases.forEach(canvas => { | |
| const initFn = canvas.dataset.initFn; | |
| if (initFn && window[initFn]) { | |
| ensureCanvasVisible(canvas.id, window[initFn]); | |
| } | |
| }); | |
| lazyObserver.unobserve(section); | |
| } | |
| }); | |
| }, observerOptions); | |
| document.querySelectorAll('.topic-section').forEach(section => { | |
| lazyObserver.observe(section); | |
| }); | |
| } | |
| // ========== INTERACTIVE ELEMENTS ========== | |
| function initInteractiveElements() { | |
| // Toggle visibility for practice answers | |
| document.querySelectorAll('.show-answers-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const answers = btn.nextElementSibling; | |
| if (answers && answers.classList.contains('practice-answers')) { | |
| const isHidden = answers.style.display === 'none' || !answers.style.display; | |
| answers.style.display = isHidden ? 'block' : 'none'; | |
| btn.textContent = isHidden ? 'Hide Answers' : 'Show Answers'; | |
| } | |
| }); | |
| }); | |
| // Initialize formula tooltips | |
| document.querySelectorAll('.formula-var').forEach(el => { | |
| el.style.cursor = 'help'; | |
| }); | |
| } | |
| // ========== KEYBOARD SHORTCUTS ========== | |
| function initKeyboardShortcuts() { | |
| document.addEventListener('keydown', (e) => { | |
| // Ctrl/Cmd + K for search | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'k') { | |
| e.preventDefault(); | |
| const searchInput = document.getElementById('globalSearch'); | |
| if (searchInput) { | |
| searchInput.focus(); | |
| } | |
| } | |
| // Arrow keys for topic navigation | |
| if (e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { | |
| e.preventDefault(); | |
| navigateTopics(e.key === 'ArrowDown' ? 1 : -1); | |
| } | |
| }); | |
| } | |
| function navigateTopics(direction) { | |
| const visibleTopics = Array.from(document.querySelectorAll('.topic-section')) | |
| .filter(t => t.style.display !== 'none'); | |
| const currentActive = document.querySelector('.topic-link.active'); | |
| let currentIndex = 0; | |
| if (currentActive) { | |
| const currentId = currentActive.getAttribute('data-topic'); | |
| currentIndex = visibleTopics.findIndex(t => | |
| t.id === currentId || t.id === `topic-${currentId}` | |
| ); | |
| } | |
| const newIndex = Math.max(0, Math.min(visibleTopics.length - 1, currentIndex + direction)); | |
| if (visibleTopics[newIndex]) { | |
| visibleTopics[newIndex].scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } | |
| } | |
| 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;'); | |
| // ===== TOGGLE ANSWER FUNCTION ===== | |
| /** | |
| * toggleAnswer - Robust Show/Hide Answer Function for practice problems | |
| * @param {string} answerId - The ID of the answer element to toggle | |
| */ | |
| function toggleAnswer(answerId) { | |
| const answerElement = document.getElementById(answerId); | |
| if (!answerElement) { | |
| console.warn(`Element with ID "${answerId}" not found.`); | |
| return; | |
| } | |
| // Find the button that triggered this | |
| const allButtons = document.querySelectorAll('.show-answer-btn'); | |
| let triggerButton = null; | |
| allButtons.forEach(btn => { | |
| const onclickAttr = btn.getAttribute('onclick'); | |
| if (onclickAttr && onclickAttr.includes(answerId)) { | |
| triggerButton = btn; | |
| } | |
| }); | |
| // Toggle display | |
| const isHidden = answerElement.style.display === 'none' || | |
| getComputedStyle(answerElement).display === 'none' || | |
| answerElement.style.display === ''; | |
| if (isHidden) { | |
| answerElement.style.display = 'block'; | |
| if (triggerButton) { | |
| triggerButton.textContent = 'Hide Answers'; | |
| triggerButton.classList.add('active'); | |
| } | |
| } else { | |
| answerElement.style.display = 'none'; | |
| if (triggerButton) { | |
| triggerButton.textContent = 'Show Answers'; | |
| triggerButton.classList.remove('active'); | |
| } | |
| } | |
| } | |
| // ===== FIX ALL SHOW ANSWER BUTTONS (EVENT DELEGATION) ===== | |
| document.addEventListener('DOMContentLoaded', function () { | |
| // Use event delegation to handle all show answer buttons | |
| document.addEventListener('click', function (e) { | |
| // Check if clicked element is a show-answers button | |
| if (e.target.classList.contains('show-answers-btn') || | |
| e.target.classList.contains('show-answer-btn')) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const btn = e.target; | |
| const answerDiv = btn.nextElementSibling; | |
| if (answerDiv && (answerDiv.classList.contains('practice-answers') || | |
| answerDiv.classList.contains('answer-content') || | |
| answerDiv.id && answerDiv.id.includes('answer'))) { | |
| // Toggle display | |
| if (answerDiv.style.display === 'none' || answerDiv.style.display === '') { | |
| answerDiv.style.display = 'block'; | |
| btn.textContent = 'Hide Answers'; | |
| btn.classList.add('active'); | |
| } else { | |
| answerDiv.style.display = 'none'; | |
| btn.textContent = 'Show Answers'; | |
| btn.classList.remove('active'); | |
| } | |
| } | |
| } | |
| }); | |
| // Ensure all answer divs start hidden | |
| document.querySelectorAll('.practice-answers, .answer-content').forEach(el => { | |
| el.style.display = 'none'; | |
| }); | |
| // Fix button text (remove line breaks) | |
| document.querySelectorAll('.show-answers-btn, .show-answer-btn').forEach(btn => { | |
| const text = btn.textContent.trim(); | |
| if (text.includes('Show') && text.includes('Answers')) { | |
| btn.textContent = 'Show Answers'; | |
| } | |
| }); | |
| console.log('%cβ Show Answer Buttons Fixed', 'color: #51cf66;'); | |
| }); | |
| // ===== TOPIC 10: SKEWNESS VISUALIZATION ===== | |
| function initSkewnessVisualization() { | |
| const canvas = document.getElementById('skewnessCanvas'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let animating = false; | |
| function normalPDF(x, mean, std) { | |
| return Math.exp(-0.5 * Math.pow((x - mean) / std, 2)) / (std * Math.sqrt(2 * Math.PI)); | |
| } | |
| function betaPDF(x, alpha, beta) { | |
| if (x <= 0 || x >= 1) return 0; | |
| const B = (Math.pow(x, alpha - 1) * Math.pow(1 - x, beta - 1)); | |
| return B * 3; // Scale for visibility | |
| } | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| const padding = 50; | |
| const chartWidth = (canvas.width - padding * 4) / 3; | |
| const chartHeight = canvas.height - padding * 3; | |
| // Title | |
| drawText(ctx, 'Types of Skewness: Where Mean, Median, and Mode Fall', canvas.width / 2, 25, 16, COLORS.cyan); | |
| const charts = [ | |
| { title: 'NEGATIVE (Left) Skew', color: '#64ffda', type: 'left', meanPos: 0.35, medianPos: 0.5, modePos: 0.65 }, | |
| { title: 'SYMMETRIC (No Skew)', color: '#4a90e2', type: 'symmetric', meanPos: 0.5, medianPos: 0.5, modePos: 0.5 }, | |
| { title: 'POSITIVE (Right) Skew', color: '#ff6b6b', type: 'right', meanPos: 0.65, medianPos: 0.5, modePos: 0.35 } | |
| ]; | |
| charts.forEach((chart, i) => { | |
| const startX = padding + i * (chartWidth + padding); | |
| const startY = padding + 30; | |
| // Draw axes | |
| drawLine(ctx, startX, startY + chartHeight, startX + chartWidth, startY + chartHeight, COLORS.text, 2); | |
| // Draw distribution curve | |
| ctx.beginPath(); | |
| ctx.strokeStyle = chart.color; | |
| ctx.lineWidth = 3; | |
| for (let px = 0; px <= chartWidth; px++) { | |
| const x = px / chartWidth; | |
| let y; | |
| if (chart.type === 'left') { | |
| y = betaPDF(x, 5, 2); | |
| } else if (chart.type === 'right') { | |
| y = betaPDF(x, 2, 5); | |
| } else { | |
| y = normalPDF(x, 0.5, 0.15) * 0.15; | |
| } | |
| const plotX = startX + px; | |
| const plotY = startY + chartHeight - y * chartHeight * 0.8; | |
| if (px === 0) ctx.moveTo(plotX, plotY); | |
| else ctx.lineTo(plotX, plotY); | |
| } | |
| ctx.stroke(); | |
| // Fill under curve | |
| ctx.globalAlpha = 0.2; | |
| ctx.fillStyle = chart.color; | |
| ctx.lineTo(startX + chartWidth, startY + chartHeight); | |
| ctx.lineTo(startX, startY + chartHeight); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.globalAlpha = 1; | |
| // Draw Mean, Median, Mode lines | |
| const measures = [ | |
| { pos: chart.modePos, label: 'Mode', color: '#ff6b6b', dash: [5, 5] }, | |
| { pos: chart.medianPos, label: 'Median', color: '#ffd93d', dash: [10, 5] }, | |
| { pos: chart.meanPos, label: 'Mean', color: '#51cf66', dash: [2, 2] } | |
| ]; | |
| measures.forEach((m, idx) => { | |
| const lineX = startX + m.pos * chartWidth; | |
| ctx.setLineDash(m.dash); | |
| drawLine(ctx, lineX, startY + 20, lineX, startY + chartHeight, m.color, 2); | |
| ctx.setLineDash([]); | |
| drawText(ctx, m.label, lineX, startY + 10 + idx * 12, 10, m.color); | |
| }); | |
| // Title | |
| drawText(ctx, chart.title, startX + chartWidth / 2, startY + chartHeight + 25, 12, chart.color); | |
| // Relationship text | |
| let relText = ''; | |
| if (chart.type === 'left') relText = 'Mean < Median < Mode'; | |
| else if (chart.type === 'right') relText = 'Mode < Median < Mean'; | |
| else relText = 'Mean = Median = Mode'; | |
| drawText(ctx, relText, startX + chartWidth / 2, startY + chartHeight + 45, 10, COLORS.text); | |
| }); | |
| } | |
| const animateBtn = document.getElementById('skewnessAnimateBtn'); | |
| const resetBtn = document.getElementById('skewnessResetBtn'); | |
| if (animateBtn) animateBtn.addEventListener('click', draw); | |
| if (resetBtn) resetBtn.addEventListener('click', draw); | |
| draw(); | |
| } | |
| // ===== TOPIC 20: PDF vs CDF VISUALIZATION ===== | |
| function initPDFCDFVisualization() { | |
| const canvas = document.getElementById('pdfCdfCanvas'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let rangeStart = 3, rangeEnd = 7; | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| const padding = 60; | |
| const chartWidth = (canvas.width - padding * 3) / 2; | |
| const chartHeight = canvas.height - padding * 2.5; | |
| // Title | |
| drawText(ctx, 'PDF vs CDF: Uniform Distribution [0, 10]', canvas.width / 2, 25, 16, COLORS.cyan); | |
| // PDF (Left side) | |
| const pdfStartX = padding; | |
| const pdfStartY = padding + 20; | |
| drawText(ctx, 'PDF: Probability Density Function', pdfStartX + chartWidth / 2, pdfStartY - 5, 14, '#64ffda'); | |
| // Axes | |
| drawLine(ctx, pdfStartX, pdfStartY + chartHeight, pdfStartX + chartWidth, pdfStartY + chartHeight, COLORS.text, 2); | |
| drawLine(ctx, pdfStartX, pdfStartY, pdfStartX, pdfStartY + chartHeight, COLORS.text, 2); | |
| // X-axis labels | |
| for (let i = 0; i <= 10; i += 2) { | |
| const x = pdfStartX + (i / 10) * chartWidth; | |
| drawText(ctx, i.toString(), x, pdfStartY + chartHeight + 15, 10, COLORS.textSecondary); | |
| } | |
| // PDF rectangle (height = 0.1 for uniform [0,10]) | |
| const pdfHeight = chartHeight * 0.4; | |
| ctx.fillStyle = 'rgba(100, 255, 218, 0.2)'; | |
| ctx.fillRect(pdfStartX, pdfStartY + chartHeight - pdfHeight, chartWidth, pdfHeight); | |
| ctx.strokeStyle = '#64ffda'; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(pdfStartX, pdfStartY + chartHeight - pdfHeight, chartWidth, pdfHeight); | |
| // Shade the selected range | |
| const rangeX1 = pdfStartX + (rangeStart / 10) * chartWidth; | |
| const rangeX2 = pdfStartX + (rangeEnd / 10) * chartWidth; | |
| ctx.fillStyle = 'rgba(255, 107, 107, 0.5)'; | |
| ctx.fillRect(rangeX1, pdfStartY + chartHeight - pdfHeight, rangeX2 - rangeX1, pdfHeight); | |
| // Labels | |
| drawText(ctx, 'Height = 0.1', pdfStartX + chartWidth + 10, pdfStartY + chartHeight - pdfHeight / 2, 10, COLORS.cyan, 'left'); | |
| const prob = ((rangeEnd - rangeStart) / 10).toFixed(2); | |
| drawText(ctx, `P(${rangeStart} β€ X β€ ${rangeEnd}) = ${prob}`, pdfStartX + chartWidth / 2, pdfStartY + chartHeight - pdfHeight - 15, 12, '#ff6b6b'); | |
| drawText(ctx, `Area = (${rangeEnd}-${rangeStart}) Γ 0.1 = ${prob}`, pdfStartX + chartWidth / 2, pdfStartY + chartHeight - pdfHeight / 2, 11, '#ff6b6b'); | |
| // CDF (Right side) | |
| const cdfStartX = padding * 2 + chartWidth; | |
| const cdfStartY = pdfStartY; | |
| drawText(ctx, 'CDF: Cumulative Distribution Function', cdfStartX + chartWidth / 2, cdfStartY - 5, 14, '#ff6b6b'); | |
| // Axes | |
| drawLine(ctx, cdfStartX, cdfStartY + chartHeight, cdfStartX + chartWidth, cdfStartY + chartHeight, COLORS.text, 2); | |
| drawLine(ctx, cdfStartX, cdfStartY, cdfStartX, cdfStartY + chartHeight, COLORS.text, 2); | |
| // X-axis labels | |
| for (let i = 0; i <= 10; i += 2) { | |
| const x = cdfStartX + (i / 10) * chartWidth; | |
| drawText(ctx, i.toString(), x, cdfStartY + chartHeight + 15, 10, COLORS.textSecondary); | |
| } | |
| // Y-axis labels | |
| for (let i = 0; i <= 1; i += 0.2) { | |
| const y = cdfStartY + chartHeight - i * chartHeight; | |
| drawText(ctx, i.toFixed(1), cdfStartX - 20, y + 4, 10, COLORS.textSecondary); | |
| } | |
| // CDF ramp line | |
| ctx.beginPath(); | |
| ctx.strokeStyle = '#ff6b6b'; | |
| ctx.lineWidth = 3; | |
| ctx.moveTo(cdfStartX, cdfStartY + chartHeight); | |
| ctx.lineTo(cdfStartX + chartWidth, cdfStartY); | |
| ctx.stroke(); | |
| // Mark points for selected range | |
| const cdfY1 = cdfStartY + chartHeight - (rangeStart / 10) * chartHeight; | |
| const cdfY2 = cdfStartY + chartHeight - (rangeEnd / 10) * chartHeight; | |
| const cdfX1 = cdfStartX + (rangeStart / 10) * chartWidth; | |
| const cdfX2 = cdfStartX + (rangeEnd / 10) * chartWidth; | |
| drawCircle(ctx, cdfX1, cdfY1, 6, '#ffd93d'); | |
| drawCircle(ctx, cdfX2, cdfY2, 6, '#ffd93d'); | |
| // Dashed lines to points | |
| ctx.setLineDash([5, 5]); | |
| drawLine(ctx, cdfX1, cdfY1, cdfX1, cdfStartY + chartHeight, '#ffd93d', 1); | |
| drawLine(ctx, cdfX2, cdfY2, cdfX2, cdfStartY + chartHeight, '#ffd93d', 1); | |
| drawLine(ctx, cdfStartX, cdfY1, cdfX1, cdfY1, '#ffd93d', 1); | |
| drawLine(ctx, cdfStartX, cdfY2, cdfX2, cdfY2, '#ffd93d', 1); | |
| ctx.setLineDash([]); | |
| drawText(ctx, `F(${rangeStart}) = ${(rangeStart / 10).toFixed(1)}`, cdfX1 + 10, cdfY1 - 10, 10, '#ffd93d', 'left'); | |
| drawText(ctx, `F(${rangeEnd}) = ${(rangeEnd / 10).toFixed(1)}`, cdfX2 + 10, cdfY2 - 10, 10, '#ffd93d', 'left'); | |
| // Relationship | |
| drawText(ctx, `P(${rangeStart}β€Xβ€${rangeEnd}) = F(${rangeEnd}) - F(${rangeStart}) = ${prob}`, | |
| cdfStartX + chartWidth / 2, cdfStartY + chartHeight / 2, 11, '#64ffda'); | |
| // Key insight | |
| drawText(ctx, 'π‘ Area under PDF = Height on CDF', canvas.width / 2, canvas.height - 20, 14, COLORS.cyan); | |
| } | |
| const slider1 = document.getElementById('pdfRangeSlider'); | |
| const slider2 = document.getElementById('pdfRangeSlider2'); | |
| const label = document.getElementById('pdfRangeLabel'); | |
| const animateBtn = document.getElementById('pdfCdfAnimateBtn'); | |
| function updateSliders() { | |
| rangeStart = parseFloat(slider1.value); | |
| rangeEnd = parseFloat(slider2.value); | |
| if (rangeEnd < rangeStart) { | |
| const temp = rangeStart; | |
| rangeStart = rangeEnd; | |
| rangeEnd = temp; | |
| } | |
| if (label) label.textContent = `[${rangeStart}, ${rangeEnd}]`; | |
| draw(); | |
| } | |
| if (slider1) slider1.addEventListener('input', updateSliders); | |
| if (slider2) slider2.addEventListener('input', updateSliders); | |
| if (animateBtn) animateBtn.addEventListener('click', draw); | |
| draw(); | |
| } | |
| // ===== TOPIC 24: NORMAL DISTRIBUTION 68-95-99.7 RULE ===== | |
| function initNormalRuleVisualization() { | |
| const canvas = document.getElementById('normalRuleCanvas'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let showLevel = 'all'; // '1', '2', '3', or 'all' | |
| function normalPDF(x) { | |
| return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI); | |
| } | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| const padding = 60; | |
| const width = canvas.width - padding * 2; | |
| const height = canvas.height - padding * 2; | |
| const centerX = canvas.width / 2; | |
| const centerY = padding + height * 0.85; | |
| const scale = width / 8; // -4 to +4 std devs | |
| // Title | |
| drawText(ctx, 'The 68-95-99.7 Rule (Empirical Rule)', canvas.width / 2, 25, 18, COLORS.cyan); | |
| // Draw axes | |
| drawLine(ctx, padding, centerY, canvas.width - padding, centerY, COLORS.text, 2); | |
| // Draw standard deviation markers | |
| for (let i = -3; i <= 3; i++) { | |
| const x = centerX + i * scale; | |
| const label = i === 0 ? 'ΞΌ' : (i > 0 ? `+${i}Ο` : `${i}Ο`); | |
| drawLine(ctx, x, centerY, x, centerY + 10, COLORS.text, 1); | |
| drawText(ctx, label, x, centerY + 25, 12, i === 0 ? '#ffd93d' : COLORS.textSecondary); | |
| } | |
| // Fill regions based on showLevel | |
| const regions = [ | |
| { sigma: 3, color: 'rgba(153, 102, 255, 0.3)', percent: '99.7%', show: showLevel === '3' || showLevel === 'all' }, | |
| { sigma: 2, color: 'rgba(255, 107, 107, 0.4)', percent: '95%', show: showLevel === '2' || showLevel === 'all' }, | |
| { sigma: 1, color: 'rgba(100, 255, 218, 0.5)', percent: '68%', show: showLevel === '1' || showLevel === 'all' } | |
| ]; | |
| regions.forEach(region => { | |
| if (!region.show) return; | |
| ctx.beginPath(); | |
| ctx.fillStyle = region.color; | |
| for (let px = -region.sigma * scale; px <= region.sigma * scale; px++) { | |
| const x = px / scale; | |
| const y = normalPDF(x); | |
| const plotX = centerX + px; | |
| const plotY = centerY - y * height * 2; | |
| if (px === -region.sigma * scale) { | |
| ctx.moveTo(plotX, centerY); | |
| ctx.lineTo(plotX, plotY); | |
| } else { | |
| ctx.lineTo(plotX, plotY); | |
| } | |
| } | |
| ctx.lineTo(centerX + region.sigma * scale, centerY); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| }); | |
| // Draw the curve | |
| ctx.beginPath(); | |
| ctx.strokeStyle = 'white'; | |
| ctx.lineWidth = 3; | |
| for (let px = -4 * scale; px <= 4 * scale; px++) { | |
| const x = px / scale; | |
| const y = normalPDF(x); | |
| const plotX = centerX + px; | |
| const plotY = centerY - y * height * 2; | |
| if (px === -4 * scale) ctx.moveTo(plotX, plotY); | |
| else ctx.lineTo(plotX, plotY); | |
| } | |
| ctx.stroke(); | |
| // Draw percentage labels | |
| const labelY = centerY - height * 0.4; | |
| if (showLevel === '1' || showLevel === 'all') { | |
| drawText(ctx, '68%', centerX, labelY + 60, 24, '#64ffda'); | |
| } | |
| if (showLevel === '2' || showLevel === 'all') { | |
| drawText(ctx, '95%', centerX, labelY + 30, 18, '#ff6b6b'); | |
| } | |
| if (showLevel === '3' || showLevel === 'all') { | |
| drawText(ctx, '99.7%', centerX, labelY, 14, '#9966ff'); | |
| } | |
| // Legend | |
| const legendY = canvas.height - 40; | |
| drawRect(ctx, padding, legendY, 15, 15, 'rgba(100, 255, 218, 0.5)'); | |
| drawText(ctx, 'Β±1Ο = 68%', padding + 25, legendY + 12, 11, COLORS.text, 'left'); | |
| drawRect(ctx, padding + 120, legendY, 15, 15, 'rgba(255, 107, 107, 0.4)'); | |
| drawText(ctx, 'Β±2Ο = 95%', padding + 145, legendY + 12, 11, COLORS.text, 'left'); | |
| drawRect(ctx, padding + 240, legendY, 15, 15, 'rgba(153, 102, 255, 0.3)'); | |
| drawText(ctx, 'Β±3Ο = 99.7%', padding + 265, legendY + 12, 11, COLORS.text, 'left'); | |
| // Example | |
| drawText(ctx, 'Example: IQ (ΞΌ=100, Ο=15) β 68% between 85-115', canvas.width - padding - 150, legendY + 12, 11, '#ffd93d'); | |
| } | |
| const show68Btn = document.getElementById('normalShow68Btn'); | |
| const show95Btn = document.getElementById('normalShow95Btn'); | |
| const show997Btn = document.getElementById('normalShow997Btn'); | |
| const showAllBtn = document.getElementById('normalShowAllBtn'); | |
| if (show68Btn) show68Btn.addEventListener('click', () => { showLevel = '1'; draw(); }); | |
| if (show95Btn) show95Btn.addEventListener('click', () => { showLevel = '2'; draw(); }); | |
| if (show997Btn) show997Btn.addEventListener('click', () => { showLevel = '3'; draw(); }); | |
| if (showAllBtn) showAllBtn.addEventListener('click', () => { showLevel = 'all'; draw(); }); | |
| draw(); | |
| } | |
| // ===== ML-10: DECISION TREE VISUALIZATION ===== | |
| function initDecisionTreeVisualization() { | |
| const canvas = document.getElementById('decisionTreeCanvas'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let testHeight = 175; | |
| let classifying = false; | |
| let classificationPath = null; | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| const centerX = canvas.width / 2; | |
| // Title | |
| drawText(ctx, 'Decision Tree: Is a Person Tall? (Height > 170 cm)', centerX, 30, 18, COLORS.cyan); | |
| // Root node (decision node) | |
| const rootX = centerX, rootY = 100; | |
| const nodeWidth = 180, nodeHeight = 60; | |
| // Draw root node | |
| ctx.fillStyle = classifying ? '#21262d' : '#1a365d'; | |
| ctx.strokeStyle = COLORS.cyan; | |
| ctx.lineWidth = 3; | |
| roundRect(ctx, rootX - nodeWidth / 2, rootY - nodeHeight / 2, nodeWidth, nodeHeight, 10, true, true); | |
| drawText(ctx, 'Height > 170?', rootX, rootY - 5, 16, COLORS.cyan); | |
| drawText(ctx, '(Root Node)', rootX, rootY + 15, 11, COLORS.textSecondary); | |
| // Branches | |
| const leafY = 280; | |
| const leftX = centerX - 180; | |
| const rightX = centerX + 180; | |
| // Left branch (NO) | |
| ctx.strokeStyle = classificationPath === 'left' ? '#ff6b6b' : COLORS.text; | |
| ctx.lineWidth = classificationPath === 'left' ? 4 : 2; | |
| ctx.beginPath(); | |
| ctx.moveTo(rootX - nodeWidth / 4, rootY + nodeHeight / 2); | |
| ctx.lineTo(leftX, leafY - nodeHeight / 2); | |
| ctx.stroke(); | |
| drawText(ctx, 'NO (β€ 170)', leftX + 40, rootY + 80, 14, '#ff6b6b'); | |
| // Right branch (YES) | |
| ctx.strokeStyle = classificationPath === 'right' ? '#51cf66' : COLORS.text; | |
| ctx.lineWidth = classificationPath === 'right' ? 4 : 2; | |
| ctx.beginPath(); | |
| ctx.moveTo(rootX + nodeWidth / 4, rootY + nodeHeight / 2); | |
| ctx.lineTo(rightX, leafY - nodeHeight / 2); | |
| ctx.stroke(); | |
| drawText(ctx, 'YES (> 170)', rightX - 40, rootY + 80, 14, '#51cf66'); | |
| // Left leaf node (NOT TALL) | |
| const leafActive = classificationPath === 'left'; | |
| ctx.fillStyle = leafActive ? '#4a1c1c' : '#21262d'; | |
| ctx.strokeStyle = '#ff6b6b'; | |
| ctx.lineWidth = leafActive ? 4 : 2; | |
| roundRect(ctx, leftX - nodeWidth / 2, leafY - nodeHeight / 2, nodeWidth, nodeHeight, 10, true, true); | |
| drawText(ctx, 'π΄ Class: 0', leftX, leafY - 8, 16, '#ff6b6b'); | |
| drawText(ctx, 'NOT TALL', leftX, leafY + 12, 12, '#ff6b6b'); | |
| // Right leaf node (TALL) | |
| const rightActive = classificationPath === 'right'; | |
| ctx.fillStyle = rightActive ? '#1c4a1c' : '#21262d'; | |
| ctx.strokeStyle = '#51cf66'; | |
| ctx.lineWidth = rightActive ? 4 : 2; | |
| roundRect(ctx, rightX - nodeWidth / 2, leafY - nodeHeight / 2, nodeWidth, nodeHeight, 10, true, true); | |
| drawText(ctx, 'π’ Class: 1', rightX, leafY - 8, 16, '#51cf66'); | |
| drawText(ctx, 'TALL', rightX, leafY + 12, 12, '#51cf66'); | |
| // Show test input | |
| if (classifying) { | |
| const resultText = testHeight > 170 ? `${testHeight} cm β TALL (Class 1)` : `${testHeight} cm β NOT TALL (Class 0)`; | |
| const resultColor = testHeight > 170 ? '#51cf66' : '#ff6b6b'; | |
| drawText(ctx, `Test Input: ${testHeight} cm`, centerX, 380, 16, COLORS.text); | |
| drawText(ctx, `Result: ${resultText}`, centerX, 410, 18, resultColor); | |
| } | |
| // Legend | |
| drawText(ctx, 'π Components: Root Node (Question) β Branches (Yes/No) β Leaf Nodes (Predictions)', centerX, canvas.height - 20, 12, COLORS.textSecondary); | |
| } | |
| function roundRect(ctx, x, y, width, height, radius, fill, stroke) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x + radius, y); | |
| ctx.lineTo(x + width - radius, y); | |
| ctx.quadraticCurveTo(x + width, y, x + width, y + radius); | |
| ctx.lineTo(x + width, y + height - radius); | |
| ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); | |
| ctx.lineTo(x + radius, y + height); | |
| ctx.quadraticCurveTo(x, y + height, x, y + height - radius); | |
| ctx.lineTo(x, y + radius); | |
| ctx.quadraticCurveTo(x, y, x + radius, y); | |
| ctx.closePath(); | |
| if (fill) ctx.fill(); | |
| if (stroke) ctx.stroke(); | |
| } | |
| const slider = document.getElementById('heightTestSlider'); | |
| const label = document.getElementById('heightTestLabel'); | |
| const classifyBtn = document.getElementById('treeClassifyBtn'); | |
| const resetBtn = document.getElementById('treeResetBtn'); | |
| if (slider) { | |
| slider.addEventListener('input', (e) => { | |
| testHeight = parseInt(e.target.value); | |
| if (label) label.textContent = testHeight; | |
| }); | |
| } | |
| if (classifyBtn) { | |
| classifyBtn.addEventListener('click', () => { | |
| classifying = true; | |
| classificationPath = testHeight > 170 ? 'right' : 'left'; | |
| draw(); | |
| }); | |
| } | |
| if (resetBtn) { | |
| resetBtn.addEventListener('click', () => { | |
| classifying = false; | |
| classificationPath = null; | |
| draw(); | |
| }); | |
| } | |
| draw(); | |
| } | |
| // ===== TOPIC 12: CORRELATION VISUALIZATION ===== | |
| function initCorrelationVisualization() { | |
| const canvas = document.getElementById('correlationCanvas'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let r = 0.7; | |
| function generateCorrelatedData(rho) { | |
| const data = []; | |
| const n = 50; | |
| for (let i = 0; i < n; i++) { | |
| const x = (Math.random() - 0.5) * 2; | |
| const noise = (Math.random() - 0.5) * 2; | |
| const y = rho * x + Math.sqrt(1 - rho * rho) * noise; | |
| data.push({ x: x * 40 + 50, y: y * 40 + 50 }); | |
| } | |
| return data; | |
| } | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| const padding = 60, width = canvas.width - padding * 2, height = canvas.height - padding * 2; | |
| const data = generateCorrelatedData(r); | |
| let statusText = ''; | |
| let color = '#fff'; | |
| if (r > 0.7) { statusText = 'Strong Positive'; color = '#51cf66'; } | |
| else if (r > 0.3) { statusText = 'Moderate Positive'; color = '#64ffda'; } | |
| else if (r > -0.3) { statusText = 'Weak / No Correlation'; color = '#ffd93d'; } | |
| else if (r > -0.7) { statusText = 'Moderate Negative'; color = '#ffa94d'; } | |
| else { statusText = 'Strong Negative'; color = '#ff6b6b'; } | |
| drawText(ctx, `r = ${r.toFixed(2)} (${statusText})`, canvas.width / 2, 30, 20, color); | |
| // Axes | |
| drawLine(ctx, padding, height + padding, width + padding, height + padding, COLORS.text, 2); | |
| drawLine(ctx, padding, padding, padding, height + padding, COLORS.text, 2); | |
| data.forEach(p => { | |
| const px = padding + (p.x / 100) * width; | |
| const py = padding + height - (p.y / 100) * height; | |
| drawCircle(ctx, px, py, 5, color); | |
| }); | |
| drawText(ctx, 'Standardized X', canvas.width / 2, canvas.height - 15, 12, COLORS.textSecondary); | |
| } | |
| const s = document.getElementById('corrSlider'), l = document.getElementById('corrLabel'); | |
| if (s) s.addEventListener('input', (e) => { | |
| r = e.target.value / 100; | |
| if (l) l.textContent = r.toFixed(2); | |
| draw(); | |
| }); | |
| draw(); | |
| } | |
| console.log('%c\u2713 Ultimate Learning Platform Fully Initialized', 'color: #51cf66; font-size: 14px; font-weight: bold;'); | |
| // ===== TOPIC 10: SKEWNESS VISUALIZATION ===== | |
| function initSkewnessVisualization() { | |
| const canvas = document.getElementById('skewnessCanvas'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| function normalPDF(x, mean, std) { | |
| return Math.exp(-0.5 * Math.pow((x - mean) / std, 2)) / (std * Math.sqrt(2 * Math.PI)); | |
| } | |
| function betaPDF(x, alpha, beta) { | |
| if (x <= 0 || x >= 1) return 0; | |
| const B = (Math.pow(x, alpha - 1) * Math.pow(1 - x, beta - 1)); | |
| return B * 3; // Scale for visibility | |
| } | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| const padding = 50; | |
| const chartWidth = (canvas.width - padding * 4) / 3; | |
| const chartHeight = canvas.height - 120; | |
| const charts = [ | |
| { type: 'negative', title: 'Negative Skew (Left Skewed)', color: '#ff6b6b', alpha: 5, beta: 2 }, | |
| { type: 'symmetric', title: 'Symmetric (Normal)', color: '#64ffda', mean: 0.5, std: 0.15 }, | |
| { type: 'positive', title: 'Positive Skew (Right Skewed)', color: '#51cf66', alpha: 2, beta: 5 } | |
| ]; | |
| charts.forEach((chart, idx) => { | |
| const startX = padding + idx * (chartWidth + padding); | |
| const centerY = canvas.height - 40; | |
| drawText(ctx, chart.title, startX + chartWidth / 2, 40, 14, chart.color); | |
| drawLine(ctx, startX, centerY, startX + chartWidth, centerY, COLORS.text, 2); | |
| drawLine(ctx, startX, centerY - chartHeight, startX, centerY, COLORS.text, 2); | |
| ctx.beginPath(); | |
| ctx.strokeStyle = chart.color; | |
| ctx.lineWidth = 3; | |
| for (let i = 0; i <= 100; i++) { | |
| const x = i / 100; | |
| let y = chart.type === 'symmetric' ? normalPDF(x, chart.mean, chart.std) : betaPDF(x, chart.alpha, chart.beta); | |
| const px = startX + x * chartWidth; | |
| const py = centerY - y * chartHeight / 3; | |
| if (i === 0) ctx.moveTo(px, py); | |
| else ctx.lineTo(px, py); | |
| } | |
| ctx.stroke(); | |
| let modePos, medianPos, meanPos; | |
| if (chart.type === 'negative') { modePos = 0.75; medianPos = 0.65; meanPos = 0.55; } | |
| else if (chart.type === 'positive') { modePos = 0.25; medianPos = 0.35; meanPos = 0.45; } | |
| else { modePos = medianPos = meanPos = 0.5; } | |
| if (chart.type === 'symmetric') { | |
| const x = startX + 0.5 * chartWidth; | |
| drawLine(ctx, x, centerY, x, centerY - chartHeight * 0.8, '#fff', 1); | |
| drawText(ctx, 'Mean=Median=Mode', x, centerY + 20, 10, '#fff'); | |
| } else { | |
| const ann = [ | |
| { pos: modePos, label: 'Mode', color: '#ff6b6b' }, | |
| { pos: medianPos, label: 'Median', color: '#ffd93d' }, | |
| { pos: meanPos, label: 'Mean', color: '#4a90e2' } | |
| ]; | |
| ann.forEach((a, i) => { | |
| const x = startX + a.pos * chartWidth; | |
| const h = centerY - (chart.type === 'negative' ? betaPDF(a.pos, 5, 2) : betaPDF(a.pos, 2, 5)) * chartHeight / 3; | |
| drawLine(ctx, x, centerY, x, h, a.color, 1); | |
| drawText(ctx, a.label, x, centerY + 15 + (i * 12), 10, a.color); | |
| }); | |
| } | |
| }); | |
| drawText(ctx, 'Rule: Skew pulls Mean towards the tail!', canvas.width / 2, canvas.height - 10, 12, COLORS.textSecondary); | |
| } | |
| const btn = document.getElementById('skewnessAnimateBtn'); | |
| if (btn) btn.addEventListener('click', draw); | |
| draw(); | |
| } | |
| // ===== TOPIC 20: PDF VS CDF VISUALIZATION ===== | |
| function initPDFCDFVisualization() { | |
| const canvas = document.getElementById('pdfCdfCanvas'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let rangeStart = 2, rangeEnd = 6; | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| const padding = 60; | |
| const chartWidth = (canvas.width - padding * 3) / 2; | |
| const chartHeight = canvas.height - 150; | |
| const pdfStartX = padding, cdfStartX = padding * 2 + chartWidth, chartStartY = 100; | |
| drawText(ctx, 'PDF: Relative Likelihood (height)', pdfStartX + chartWidth / 2, 50, 16, COLORS.cyan); | |
| drawLine(ctx, pdfStartX, chartStartY + chartHeight, pdfStartX + chartWidth, chartStartY + chartHeight, COLORS.text, 2); | |
| drawLine(ctx, pdfStartX, chartStartY, pdfStartX, chartStartY + chartHeight, COLORS.text, 2); | |
| ctx.fillStyle = 'rgba(100, 255, 218, 0.2)'; | |
| ctx.fillRect(pdfStartX, chartStartY + chartHeight * 0.7, chartWidth, chartHeight * 0.3); | |
| ctx.strokeStyle = COLORS.cyan; | |
| ctx.strokeRect(pdfStartX, chartStartY + chartHeight * 0.7, chartWidth, chartHeight * 0.3); | |
| const shadeX1 = pdfStartX + (rangeStart / 10) * chartWidth; | |
| const shadeX2 = pdfStartX + (rangeEnd / 10) * chartWidth; | |
| ctx.fillStyle = 'rgba(255, 107, 107, 0.5)'; | |
| ctx.fillRect(shadeX1, chartStartY + chartHeight * 0.7, shadeX2 - shadeX1, chartHeight * 0.3); | |
| drawText(ctx, 'Area = Probability', (shadeX1 + shadeX2) / 2, chartStartY + chartHeight * 0.85, 12, '#ff6b6b'); | |
| drawText(ctx, 'CDF: Cumulative Probability (area)', cdfStartX + chartWidth / 2, 50, 16, COLORS.orange); | |
| drawLine(ctx, cdfStartX, chartStartY + chartHeight, cdfStartX + chartWidth, chartStartY + chartHeight, COLORS.text, 2); | |
| drawLine(ctx, cdfStartX, chartStartY, cdfStartX, chartStartY + chartHeight, COLORS.text, 2); | |
| ctx.beginPath(); | |
| ctx.strokeStyle = COLORS.orange; ctx.lineWidth = 3; | |
| ctx.moveTo(cdfStartX, chartStartY + chartHeight); ctx.lineTo(cdfStartX + chartWidth, chartStartY); | |
| ctx.stroke(); | |
| const cdfX1 = cdfStartX + (rangeStart / 10) * chartWidth; | |
| const cdfY1 = chartStartY + chartHeight - (rangeStart / 10) * chartHeight; | |
| const cdfX2 = cdfStartX + (rangeEnd / 10) * chartWidth; | |
| const cdfY2 = chartStartY + chartHeight - (rangeEnd / 10) * chartHeight; | |
| drawCircle(ctx, cdfX1, cdfY1, 6, '#ffd93d'); | |
| drawCircle(ctx, cdfX2, cdfY2, 6, '#ffd93d'); | |
| ctx.setLineDash([5, 5]); | |
| drawLine(ctx, cdfX1, cdfY1, cdfX1, chartStartY + chartHeight, '#ffd93d', 1); | |
| drawLine(ctx, cdfX2, cdfY2, cdfX2, chartStartY + chartHeight, '#ffd93d', 1); | |
| ctx.setLineDash([]); | |
| drawText(ctx, `F(${rangeStart}) = ${(rangeStart / 10).toFixed(1)}`, cdfX1, cdfY1 - 10, 10, '#ffd93d'); | |
| drawText(ctx, `F(${rangeEnd}) = ${(rangeEnd / 10).toFixed(1)}`, cdfX2, cdfY2 - 10, 10, '#ffd93d'); | |
| } | |
| const s1 = document.getElementById('pdfRangeSlider'), s2 = document.getElementById('pdfRangeSlider2'), l = document.getElementById('pdfRangeLabel'); | |
| const update = () => { rangeStart = Math.min(s1.value, s2.value); rangeEnd = Math.max(s1.value, s2.value); if (l) l.textContent = `Range: [${rangeStart}, ${rangeEnd}]`; draw(); }; | |
| if (s1) s1.addEventListener('input', update); if (s2) s2.addEventListener('input', update); | |
| draw(); | |
| } | |
| // ===== TOPIC 24: NORMAL DISTRIBUTION 68-95-99.7 RULE ===== | |
| function initNormalRuleVisualization() { | |
| const canvas = document.getElementById('normalRuleCanvas'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let showLevel = 'all'; | |
| function normalPDF(x) { return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI); } | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| const padding = 60, width = canvas.width - padding * 2, height = canvas.height - padding * 2; | |
| const centerX = canvas.width / 2, centerY = padding + height * 0.85, scale = width / 8; | |
| drawText(ctx, 'The 68-95-99.7 Rule (Empirical Rule)', centerX, 25, 18, COLORS.cyan); | |
| drawLine(ctx, padding, centerY, canvas.width - padding, centerY, COLORS.text, 2); | |
| for (let i = -3; i <= 3; i++) { | |
| const x = centerX + i * scale; | |
| const label = i === 0 ? '\u03bc' : (i > 0 ? `+${i}\u03c3` : `${i}\u03c3`); | |
| drawLine(ctx, x, centerY, x, centerY + 10, COLORS.text, 1); | |
| drawText(ctx, label, x, centerY + 25, 12, i === 0 ? '#ffd93d' : COLORS.textSecondary); | |
| } | |
| const regions = [ | |
| { sigma: 3, color: 'rgba(153, 102, 255, 0.3)', show: showLevel === '3' || showLevel === 'all' }, | |
| { sigma: 2, color: 'rgba(255, 107, 107, 0.4)', show: showLevel === '2' || showLevel === 'all' }, | |
| { sigma: 1, color: 'rgba(100, 255, 218, 0.5)', show: showLevel === '1' || showLevel === 'all' } | |
| ]; | |
| regions.forEach(r => { | |
| if (!r.show) return; | |
| ctx.beginPath(); ctx.fillStyle = r.color; | |
| for (let px = -r.sigma * scale; px <= r.sigma * scale; px++) { | |
| const x = px / scale, y = normalPDF(x); | |
| const plotX = centerX + px, plotY = centerY - y * height * 2; | |
| if (px === -r.sigma * scale) ctx.moveTo(plotX, centerY); | |
| ctx.lineTo(plotX, plotY); | |
| } | |
| ctx.lineTo(centerX + r.sigma * scale, centerY); ctx.closePath(); ctx.fill(); | |
| }); | |
| ctx.beginPath(); ctx.strokeStyle = 'white'; ctx.lineWidth = 3; | |
| for (let px = -4 * scale; px <= 4 * scale; px++) { | |
| const x = px / scale, y = normalPDF(x); | |
| const plotX = centerX + px, plotY = centerY - y * height * 2; | |
| if (px === -4 * scale) ctx.moveTo(plotX, plotY); | |
| else ctx.lineTo(plotX, plotY); | |
| } | |
| ctx.stroke(); | |
| } | |
| const b1 = document.getElementById('normalShow68Btn'), b2 = document.getElementById('normalShow95Btn'), b3 = document.getElementById('normalShow997Btn'), b4 = document.getElementById('normalShowAllBtn'); | |
| if (b1) b1.addEventListener('click', () => { showLevel = '1'; draw(); }); | |
| if (b2) b2.addEventListener('click', () => { showLevel = '2'; draw(); }); | |
| if (b3) b3.addEventListener('click', () => { showLevel = '3'; draw(); }); | |
| if (b4) b4.addEventListener('click', () => { showLevel = 'all'; draw(); }); | |
| draw(); | |
| } | |
| // ===== ML-10: DECISION TREE VISUALIZATION ===== | |
| function initDecisionTreeVisualization() { | |
| const canvas = document.getElementById('decisionTreeCanvas'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let testHeight = 165, classifying = false, classificationPath = null; | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| const centerX = canvas.width / 2, rootY = 80, leafY = 280, leftX = centerX - 180, rightX = centerX + 180; | |
| const nodeWidth = 140, nodeHeight = 60; | |
| drawLine(ctx, centerX, rootY, leftX, leafY, COLORS.textSecondary, 2); | |
| drawLine(ctx, centerX, rootY, rightX, leafY, COLORS.textSecondary, 2); | |
| ctx.fillStyle = '#21262d'; ctx.strokeStyle = COLORS.cyan; ctx.lineWidth = 3; | |
| roundRect(ctx, centerX - nodeWidth / 2, rootY - nodeHeight / 2, nodeWidth, nodeHeight, 10, true, true); | |
| drawText(ctx, 'ROOT NODE: Height > 170?', centerX, rootY + 5, 14, '#fff'); | |
| const lActive = classificationPath === 'left', rActive = classificationPath === 'right'; | |
| ctx.fillStyle = lActive ? '#4a1c1c' : '#21262d'; ctx.strokeStyle = '#ff6b6b'; | |
| roundRect(ctx, leftX - nodeWidth / 2, leafY - nodeHeight / 2, nodeWidth, nodeHeight, 10, true, true); | |
| drawText(ctx, 'Class 0: NOT TALL', leftX, leafY + 5, 14, '#ff6b6b'); | |
| ctx.fillStyle = rActive ? '#1c4a1c' : '#21262d'; ctx.strokeStyle = '#51cf66'; | |
| roundRect(ctx, rightX - nodeWidth / 2, leafY - nodeHeight / 2, nodeWidth, nodeHeight, 10, true, true); | |
| drawText(ctx, 'Class 1: TALL', rightX, leafY + 5, 14, '#51cf66'); | |
| if (classifying) { | |
| drawText(ctx, `Test: ${testHeight} cm \u2192 ${testHeight > 170 ? 'TALL' : 'NOT TALL'}`, centerX, 400, 18, COLORS.cyan); | |
| } | |
| } | |
| function roundRect(ctx, x, y, width, height, radius, fill, stroke) { | |
| ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); | |
| ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); | |
| ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); | |
| ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); | |
| ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); | |
| if (fill) ctx.fill(); if (stroke) ctx.stroke(); | |
| } | |
| const s = document.getElementById('heightTestSlider'), b = document.getElementById('treeClassifyBtn'); | |
| if (s) s.addEventListener('input', (e) => { testHeight = e.target.value; draw(); }); | |
| if (b) b.addEventListener('click', () => { classifying = true; classificationPath = testHeight > 170 ? 'right' : 'left'; draw(); }); | |
| draw(); | |
| } | |
| // ===== TOPIC 11: COVARIANCE VISUALIZATION ===== | |
| function initCovarianceVisualization() { | |
| const canvas = document.getElementById('covarianceCanvas'); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let covType = 'positive'; | |
| function generateData(type) { | |
| let data = []; | |
| for (let i = 0; i < 30; i++) { | |
| let x = Math.random() * 80 + 10, y; | |
| if (type === 'positive') y = x + (Math.random() - 0.5) * 30; | |
| else if (type === 'negative') y = 100 - x + (Math.random() - 0.5) * 30; | |
| else y = Math.random() * 80 + 10; | |
| data.push({ x, y }); | |
| } | |
| return data; | |
| } | |
| function draw() { | |
| clearCanvas(ctx, canvas); | |
| const padding = 60, width = canvas.width - padding * 2, height = canvas.height - padding * 2; | |
| const data = generateData(covType); | |
| const meanX = data.reduce((s, p) => s + p.x, 0) / data.length; | |
| const meanY = data.reduce((s, p) => s + p.y, 0) / data.length; | |
| const cov = data.reduce((s, p) => s + (p.x - meanX) * (p.y - meanY), 0) / (data.length - 1); | |
| drawText(ctx, `${covType.toUpperCase()} Covariance: ${cov.toFixed(2)}`, canvas.width / 2, 25, 18, COLORS.cyan); | |
| drawLine(ctx, padding, height + padding, width + padding, height + padding, COLORS.text, 2); | |
| drawLine(ctx, padding, padding, padding, height + padding, COLORS.text, 2); | |
| data.forEach(p => { | |
| drawCircle(ctx, padding + (p.x / 100) * width, padding + height - (p.y / 100) * height, 6, COLORS.cyan); | |
| }); | |
| } | |
| const b1 = document.getElementById('covPositiveBtn'), b2 = document.getElementById('covNegativeBtn'), b3 = document.getElementById('covZeroBtn'); | |
| if (b1) b1.addEventListener('click', () => { covType = 'positive'; draw(); }); | |
| if (b2) b2.addEventListener('click', () => { covType = 'negative'; draw(); }); | |
| if (b3) b3.addEventListener('click', () => { covType = 'zero'; draw(); }); | |
| draw(); | |
| } |