Aashish34's picture
Upload 3 files
3117c7d verified
// Data
const data = {
linearRegression: [
{ experience: 1, salary: 39.764 },
{ experience: 2, salary: 48.900 },
{ experience: 3, salary: 56.978 },
{ experience: 4, salary: 68.290 },
{ experience: 5, salary: 77.867 },
{ experience: 6, salary: 85.022 }
],
logistic: [
{ height: 150, label: 0, prob: 0.2 },
{ height: 160, label: 0, prob: 0.35 },
{ height: 170, label: 0, prob: 0.5 },
{ height: 180, label: 1, prob: 0.65 },
{ height: 190, label: 1, prob: 0.8 },
{ height: 200, label: 1, prob: 0.9 }
],
svm: [
{ label: 'A', x1: 2, x2: 7, class: 1 },
{ label: 'B', x1: 3, x2: 8, class: 1 },
{ label: 'C', x1: 4, x2: 7, class: 1 },
{ label: 'D', x1: 6, x2: 2, class: -1 },
{ label: 'E', x1: 7, x2: 3, class: -1 },
{ label: 'F', x1: 8, x2: 2, class: -1 }
],
knn: [
{ x: 1, y: 2, class: 'orange' },
{ x: 0.9, y: 1.7, class: 'orange' },
{ x: 1.5, y: 2.5, class: 'orange' },
{ x: 4, y: 5, class: 'yellow' },
{ x: 4.2, y: 4.8, class: 'yellow' },
{ x: 3.8, y: 5.2, class: 'yellow' }
],
roc: [
{ id: 'A', true_label: 1, score: 0.95 },
{ id: 'B', true_label: 0, score: 0.70 },
{ id: 'C', true_label: 1, score: 0.60 },
{ id: 'D', true_label: 0, score: 0.40 },
{ id: 'E', true_label: 1, score: 0.20 }
]
};
// State
let state = {
slope: 7.5,
intercept: 32,
learningRate: 0.1,
gdIterations: [],
testPoint: { x: 2, y: 1 },
svm: {
w1: 1,
w2: 1,
b: -10,
C: 1,
kernel: 'linear',
kernelParam: 1,
training: {
w: [0, 0],
b: 0,
step: 0,
learningRate: 0.01,
isTraining: false
}
}
};
// Initialize collapsible sections
function initSections() {
const sections = document.querySelectorAll('.section');
sections.forEach(section => {
const header = section.querySelector('.section-header');
const toggle = section.querySelector('.section-toggle');
const body = section.querySelector('.section-body');
// Start with first section expanded
if (section.id === 'intro') {
body.classList.add('expanded');
toggle.classList.remove('collapsed');
} else {
toggle.classList.add('collapsed');
}
header.addEventListener('click', () => {
const isExpanded = body.classList.contains('expanded');
if (isExpanded) {
body.classList.remove('expanded');
toggle.classList.add('collapsed');
} else {
body.classList.add('expanded');
toggle.classList.remove('collapsed');
// Initialize visualizations when section opens
if (section.id === 'linear-regression') initLinearRegression();
if (section.id === 'gradient-descent') initGradientDescent();
if (section.id === 'logistic-regression') initLogistic();
if (section.id === 'svm') initSVM();
if (section.id === 'knn') initKNN();
if (section.id === 'model-evaluation') initModelEvaluation();
if (section.id === 'regularization') initRegularization();
if (section.id === 'bias-variance') initBiasVariance();
if (section.id === 'cross-validation') initCrossValidation();
if (section.id === 'preprocessing') initPreprocessing();
if (section.id === 'loss-functions') initLossFunctions();
}
});
});
}
// Smooth scroll for TOC links
function initTOCLinks() {
const links = document.querySelectorAll('.toc-link');
links.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const targetId = link.getAttribute('href').substring(1);
const target = document.getElementById(targetId);
if (target) {
// Remove active from all links
links.forEach(l => l.classList.remove('active'));
link.classList.add('active');
// Scroll to target
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
// Expand the section
const toggle = target.querySelector('.section-toggle');
const body = target.querySelector('.section-body');
body.classList.add('expanded');
toggle.classList.remove('collapsed');
}
});
});
// Update active link on scroll
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
window.requestAnimationFrame(() => {
updateActiveLink();
ticking = false;
});
ticking = true;
}
});
}
function updateActiveLink() {
const sections = document.querySelectorAll('.section');
const scrollPos = window.scrollY + 100;
sections.forEach(section => {
const top = section.offsetTop;
const height = section.offsetHeight;
const id = section.getAttribute('id');
if (scrollPos >= top && scrollPos < top + height) {
document.querySelectorAll('.toc-link').forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + id) {
link.classList.add('active');
}
});
}
});
}
// Linear Regression Visualization
function initLinearRegression() {
const canvas = document.getElementById('lr-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
const slopeSlider = document.getElementById('slope-slider');
const interceptSlider = document.getElementById('intercept-slider');
const slopeVal = document.getElementById('slope-val');
const interceptVal = document.getElementById('intercept-val');
if (slopeSlider) {
slopeSlider.addEventListener('input', (e) => {
state.slope = parseFloat(e.target.value);
slopeVal.textContent = state.slope.toFixed(1);
drawLinearRegression();
});
}
if (interceptSlider) {
interceptSlider.addEventListener('input', (e) => {
state.intercept = parseFloat(e.target.value);
interceptVal.textContent = state.intercept.toFixed(1);
drawLinearRegression();
});
}
drawLinearRegression();
}
function drawLinearRegression() {
const canvas = document.getElementById('lr-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 400;
ctx.clearRect(0, 0, width, height);
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
const xMin = 0, xMax = 7;
const yMin = 0, yMax = 100;
// Draw axes
ctx.strokeStyle = '#2a3544';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding);
ctx.stroke();
// Grid
ctx.strokeStyle = 'rgba(42, 53, 68, 0.3)';
ctx.lineWidth = 1;
for (let i = 0; i <= 5; i++) {
const x = padding + (chartWidth / 5) * i;
ctx.beginPath();
ctx.moveTo(x, padding);
ctx.lineTo(x, height - padding);
ctx.stroke();
const y = height - padding - (chartHeight / 5) * i;
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
}
const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * chartWidth;
const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * chartHeight;
// Draw data points
ctx.fillStyle = '#6aa9ff';
data.linearRegression.forEach(point => {
const x = scaleX(point.experience);
const y = scaleY(point.salary);
ctx.beginPath();
ctx.arc(x, y, 6, 0, 2 * Math.PI);
ctx.fill();
});
// Draw regression line
ctx.strokeStyle = '#ff8c6a';
ctx.lineWidth = 3;
ctx.beginPath();
const y1 = state.slope * xMin + state.intercept;
const y2 = state.slope * xMax + state.intercept;
ctx.moveTo(scaleX(xMin), scaleY(y1));
ctx.lineTo(scaleX(xMax), scaleY(y2));
ctx.stroke();
// Labels
ctx.fillStyle = '#a9b4c2';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Experience (years)', width / 2, height - 20);
ctx.save();
ctx.translate(20, height / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Salary ($k)', 0, 0);
ctx.restore();
// Calculate MSE
let mse = 0;
data.linearRegression.forEach(point => {
const predicted = state.slope * point.experience + state.intercept;
const error = point.salary - predicted;
mse += error * error;
});
mse /= data.linearRegression.length;
// Display MSE
ctx.fillStyle = '#7ef0d4';
ctx.font = '14px sans-serif';
ctx.textAlign = 'right';
ctx.fillText(`MSE: ${mse.toFixed(2)}`, width - padding, padding + 20);
}
// Gradient Descent Visualization
function initGradientDescent() {
const canvas = document.getElementById('gd-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
const runBtn = document.getElementById('run-gd');
const resetBtn = document.getElementById('reset-gd');
const lrSlider = document.getElementById('lr-slider');
const lrVal = document.getElementById('lr-val');
if (lrSlider) {
lrSlider.addEventListener('input', (e) => {
state.learningRate = parseFloat(e.target.value);
lrVal.textContent = state.learningRate.toFixed(2);
});
}
if (runBtn) {
runBtn.addEventListener('click', runGradientDescent);
}
if (resetBtn) {
resetBtn.addEventListener('click', () => {
state.gdIterations = [];
drawGradientDescent();
});
}
drawGradientDescent();
}
function runGradientDescent() {
state.gdIterations = [];
let m = 0, c = 20; // Start with poor values
const alpha = state.learningRate;
const iterations = 50;
for (let i = 0; i < iterations; i++) {
let dm = 0, dc = 0;
const n = data.linearRegression.length;
// Calculate gradients
data.linearRegression.forEach(point => {
const predicted = m * point.experience + c;
const error = predicted - point.salary;
dm += (2 / n) * error * point.experience;
dc += (2 / n) * error;
});
// Update parameters
m -= alpha * dm;
c -= alpha * dc;
// Calculate loss
let loss = 0;
data.linearRegression.forEach(point => {
const predicted = m * point.experience + c;
const error = point.salary - predicted;
loss += error * error;
});
loss /= n;
state.gdIterations.push({ m, c, loss });
}
animateGradientDescent();
}
function animateGradientDescent() {
let step = 0;
const interval = setInterval(() => {
if (step >= state.gdIterations.length) {
clearInterval(interval);
return;
}
const iteration = state.gdIterations[step];
state.slope = iteration.m;
state.intercept = iteration.c;
// Update linear regression chart
drawLinearRegression();
drawGradientDescent(step);
step++;
}, 50);
}
function drawGradientDescent(currentStep = -1) {
const canvas = document.getElementById('gd-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 400;
ctx.clearRect(0, 0, width, height);
if (state.gdIterations.length === 0) {
ctx.fillStyle = '#a9b4c2';
ctx.font = '16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Click "Run Gradient Descent" to see the algorithm in action', width / 2, height / 2);
return;
}
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
const maxLoss = Math.max(...state.gdIterations.map(i => i.loss));
const minLoss = Math.min(...state.gdIterations.map(i => i.loss));
// Draw axes
ctx.strokeStyle = '#2a3544';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding);
ctx.stroke();
const scaleX = (i) => padding + (i / (state.gdIterations.length - 1)) * chartWidth;
const scaleY = (loss) => height - padding - ((loss - minLoss) / (maxLoss - minLoss)) * chartHeight;
// Draw loss curve
ctx.strokeStyle = '#7ef0d4';
ctx.lineWidth = 2;
ctx.beginPath();
state.gdIterations.forEach((iter, i) => {
const x = scaleX(i);
const y = scaleY(iter.loss);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
// Highlight current step
if (currentStep >= 0 && currentStep < state.gdIterations.length) {
const iter = state.gdIterations[currentStep];
const x = scaleX(currentStep);
const y = scaleY(iter.loss);
ctx.fillStyle = '#ff8c6a';
ctx.beginPath();
ctx.arc(x, y, 6, 0, 2 * Math.PI);
ctx.fill();
// Display current values
ctx.fillStyle = '#e8eef6';
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`Step: ${currentStep + 1}`, padding + 10, padding + 20);
ctx.fillText(`Loss: ${iter.loss.toFixed(2)}`, padding + 10, padding + 40);
}
// Labels
ctx.fillStyle = '#a9b4c2';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Iterations', width / 2, height - 20);
ctx.save();
ctx.translate(20, height / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Loss (MSE)', 0, 0);
ctx.restore();
}
// Initialize everything when DOM is ready
function init() {
initSections();
initTOCLinks();
// Initialize first section visualizations
setTimeout(() => {
initLinearRegression();
}, 100);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// SVM Visualizations
function initSVM() {
initSVMBasic();
initSVMMargin();
initSVMCParameter();
initSVMTraining();
initSVMKernel();
}
function initSVMBasic() {
const canvas = document.getElementById('svm-basic-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
const w1Slider = document.getElementById('svm-w1-slider');
const w2Slider = document.getElementById('svm-w2-slider');
const bSlider = document.getElementById('svm-b-slider');
if (w1Slider) {
w1Slider.addEventListener('input', (e) => {
state.svm.w1 = parseFloat(e.target.value);
document.getElementById('svm-w1-val').textContent = state.svm.w1.toFixed(1);
drawSVMBasic();
});
}
if (w2Slider) {
w2Slider.addEventListener('input', (e) => {
state.svm.w2 = parseFloat(e.target.value);
document.getElementById('svm-w2-val').textContent = state.svm.w2.toFixed(1);
drawSVMBasic();
});
}
if (bSlider) {
bSlider.addEventListener('input', (e) => {
state.svm.b = parseFloat(e.target.value);
document.getElementById('svm-b-val').textContent = state.svm.b.toFixed(1);
drawSVMBasic();
});
}
drawSVMBasic();
}
function drawSVMBasic() {
const canvas = document.getElementById('svm-basic-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 450;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
const xMin = 0, xMax = 10;
const yMin = 0, yMax = 10;
const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * chartWidth;
const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * chartHeight;
// Draw grid
ctx.strokeStyle = 'rgba(42, 53, 68, 0.5)';
ctx.lineWidth = 1;
for (let i = 0; i <= 10; i++) {
const x = scaleX(i);
const y = scaleY(i);
ctx.beginPath();
ctx.moveTo(x, padding);
ctx.lineTo(x, height - padding);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
}
// Draw axes
ctx.strokeStyle = '#2a3544';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding);
ctx.stroke();
// Draw decision boundary
const w1 = state.svm.w1;
const w2 = state.svm.w2;
const b = state.svm.b;
if (Math.abs(w2) > 0.01) {
ctx.strokeStyle = '#6aa9ff';
ctx.lineWidth = 3;
ctx.beginPath();
const x1 = xMin;
const y1 = -(w1 * x1 + b) / w2;
const x2 = xMax;
const y2 = -(w1 * x2 + b) / w2;
ctx.moveTo(scaleX(x1), scaleY(y1));
ctx.lineTo(scaleX(x2), scaleY(y2));
ctx.stroke();
}
// Draw data points
data.svm.forEach(point => {
const x = scaleX(point.x1);
const y = scaleY(point.x2);
const score = w1 * point.x1 + w2 * point.x2 + b;
ctx.fillStyle = point.class === 1 ? '#7ef0d4' : '#ff8c6a';
ctx.beginPath();
ctx.arc(x, y, 8, 0, 2 * Math.PI);
ctx.fill();
ctx.strokeStyle = '#1a2332';
ctx.lineWidth = 2;
ctx.stroke();
// Label
ctx.fillStyle = '#e8eef6';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(point.label, x, y - 15);
// Score
ctx.font = '11px monospace';
ctx.fillStyle = '#a9b4c2';
ctx.fillText(score.toFixed(2), x, y + 20);
});
// Labels
ctx.fillStyle = '#a9b4c2';
ctx.font = '13px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('X₁', width / 2, height - 20);
ctx.save();
ctx.translate(20, height / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('X₂', 0, 0);
ctx.restore();
// Equation
ctx.fillStyle = '#7ef0d4';
ctx.font = '14px monospace';
ctx.textAlign = 'left';
ctx.fillText(`w·x + b = ${w1.toFixed(1)}x₁ + ${w2.toFixed(1)}x₂ + ${b.toFixed(1)}`, padding + 10, padding + 25);
}
function initSVMMargin() {
const canvas = document.getElementById('svm-margin-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
drawSVMMargin();
}
function drawSVMMargin() {
const canvas = document.getElementById('svm-margin-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 450;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
const xMin = 0, xMax = 10;
const yMin = 0, yMax = 10;
const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * chartWidth;
const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * chartHeight;
// Use good values for visualization
const w1 = 0.5, w2 = -1, b = 5.5;
// Draw margin lines
if (Math.abs(w2) > 0.01) {
// Positive margin line
ctx.strokeStyle = '#ff8c6a';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.beginPath();
let x1 = xMin, y1 = -(w1 * x1 + b - 1) / w2;
let x2 = xMax, y2 = -(w1 * x2 + b - 1) / w2;
ctx.moveTo(scaleX(x1), scaleY(y1));
ctx.lineTo(scaleX(x2), scaleY(y2));
ctx.stroke();
// Negative margin line
ctx.beginPath();
y1 = -(w1 * x1 + b + 1) / w2;
y2 = -(w1 * x2 + b + 1) / w2;
ctx.moveTo(scaleX(x1), scaleY(y1));
ctx.lineTo(scaleX(x2), scaleY(y2));
ctx.stroke();
// Decision boundary
ctx.setLineDash([]);
ctx.strokeStyle = '#6aa9ff';
ctx.lineWidth = 3;
ctx.beginPath();
y1 = -(w1 * x1 + b) / w2;
y2 = -(w1 * x2 + b) / w2;
ctx.moveTo(scaleX(x1), scaleY(y1));
ctx.lineTo(scaleX(x2), scaleY(y2));
ctx.stroke();
}
// Draw data points
data.svm.forEach(point => {
const x = scaleX(point.x1);
const y = scaleY(point.x2);
const score = w1 * point.x1 + w2 * point.x2 + b;
const isSupport = Math.abs(Math.abs(score) - 1) < 0.5;
ctx.fillStyle = point.class === 1 ? '#7ef0d4' : '#ff8c6a';
ctx.beginPath();
ctx.arc(x, y, 8, 0, 2 * Math.PI);
ctx.fill();
// Highlight support vectors
if (isSupport) {
ctx.strokeStyle = '#7ef0d4';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(x, y, 14, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.fillStyle = '#e8eef6';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(point.label, x, y - 20);
});
// Show margin width
const wNorm = Math.sqrt(w1 * w1 + w2 * w2);
const marginWidth = 2 / wNorm;
ctx.fillStyle = '#7ef0d4';
ctx.font = '16px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`Margin Width: ${marginWidth.toFixed(2)}`, padding + 10, padding + 25);
ctx.fillText('Support vectors highlighted with cyan ring', padding + 10, padding + 50);
}
function initSVMCParameter() {
const canvas = document.getElementById('svm-c-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
const cSlider = document.getElementById('svm-c-slider');
if (cSlider) {
cSlider.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
state.svm.C = Math.pow(10, val);
document.getElementById('svm-c-val').textContent = state.svm.C.toFixed(state.svm.C < 10 ? 1 : 0);
drawSVMCParameter();
});
}
drawSVMCParameter();
}
function drawSVMCParameter() {
const canvas = document.getElementById('svm-c-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 450;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
const xMin = 0, xMax = 10;
const yMin = 0, yMax = 10;
const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * chartWidth;
const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * chartHeight;
// Adjust margin based on C
const C = state.svm.C;
const marginFactor = Math.min(1, 10 / C);
const w1 = 0.5 * marginFactor, w2 = -1 * marginFactor, b = 5.5;
// Calculate violations
let violations = 0;
data.svm.forEach(point => {
const score = w1 * point.x1 + w2 * point.x2 + b;
if (point.class * score < 1) violations++;
});
// Draw margin lines
if (Math.abs(w2) > 0.01) {
ctx.strokeStyle = '#ff8c6a';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.beginPath();
let x1 = xMin, y1 = -(w1 * x1 + b - 1) / w2;
let x2 = xMax, y2 = -(w1 * x2 + b - 1) / w2;
ctx.moveTo(scaleX(x1), scaleY(y1));
ctx.lineTo(scaleX(x2), scaleY(y2));
ctx.stroke();
ctx.beginPath();
y1 = -(w1 * x1 + b + 1) / w2;
y2 = -(w1 * x2 + b + 1) / w2;
ctx.moveTo(scaleX(x1), scaleY(y1));
ctx.lineTo(scaleX(x2), scaleY(y2));
ctx.stroke();
ctx.setLineDash([]);
ctx.strokeStyle = '#6aa9ff';
ctx.lineWidth = 3;
ctx.beginPath();
y1 = -(w1 * x1 + b) / w2;
y2 = -(w1 * x2 + b) / w2;
ctx.moveTo(scaleX(x1), scaleY(y1));
ctx.lineTo(scaleX(x2), scaleY(y2));
ctx.stroke();
}
// Draw points
data.svm.forEach(point => {
const x = scaleX(point.x1);
const y = scaleY(point.x2);
const score = w1 * point.x1 + w2 * point.x2 + b;
const violates = point.class * score < 1;
ctx.fillStyle = point.class === 1 ? '#7ef0d4' : '#ff8c6a';
ctx.beginPath();
ctx.arc(x, y, 8, 0, 2 * Math.PI);
ctx.fill();
if (violates) {
ctx.strokeStyle = '#ff4444';
ctx.lineWidth = 3;
ctx.stroke();
}
});
// Update info
const wNorm = Math.sqrt(w1 * w1 + w2 * w2);
const marginWidth = 2 / wNorm;
document.getElementById('margin-width').textContent = marginWidth.toFixed(2);
document.getElementById('violations-count').textContent = violations;
}
function initSVMTraining() {
const canvas = document.getElementById('svm-train-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
const trainBtn = document.getElementById('svm-train-btn');
const stepBtn = document.getElementById('svm-step-btn');
const resetBtn = document.getElementById('svm-reset-btn');
if (trainBtn) {
trainBtn.addEventListener('click', () => {
state.svm.training.step = 0;
state.svm.training.w = [0, 0];
state.svm.training.b = 0;
state.svm.training.isTraining = true;
autoTrain();
});
}
if (stepBtn) {
stepBtn.addEventListener('click', () => {
if (state.svm.training.step < data.svm.length) {
trainStep();
}
});
}
if (resetBtn) {
resetBtn.addEventListener('click', () => {
state.svm.training.step = 0;
state.svm.training.w = [0, 0];
state.svm.training.b = 0;
state.svm.training.isTraining = false;
updateTrainingInfo();
drawSVMTraining();
});
}
drawSVMTraining();
}
function trainStep() {
if (state.svm.training.step >= data.svm.length) return;
const point = data.svm[state.svm.training.step];
const w = state.svm.training.w;
const b = state.svm.training.b;
const lr = state.svm.training.learningRate;
const C = 1;
const score = w[0] * point.x1 + w[1] * point.x2 + b;
const violation = point.class * score < 1;
if (violation) {
w[0] = w[0] - lr * (w[0] - C * point.class * point.x1);
w[1] = w[1] - lr * (w[1] - C * point.class * point.x2);
state.svm.training.b = b + lr * C * point.class;
} else {
w[0] = w[0] - lr * w[0];
w[1] = w[1] - lr * w[1];
}
state.svm.training.step++;
updateTrainingInfo(point, violation);
drawSVMTraining();
}
function autoTrain() {
if (!state.svm.training.isTraining) return;
if (state.svm.training.step < data.svm.length) {
trainStep();
setTimeout(autoTrain, 800);
} else {
state.svm.training.isTraining = false;
}
}
function updateTrainingInfo(point = null, violation = null) {
document.getElementById('train-step').textContent = state.svm.training.step;
document.getElementById('train-point').textContent = point ? `${point.label} (${point.x1}, ${point.x2})` : '-';
document.getElementById('train-w').textContent = `${state.svm.training.w[0].toFixed(2)}, ${state.svm.training.w[1].toFixed(2)}`;
document.getElementById('train-b').textContent = state.svm.training.b.toFixed(2);
document.getElementById('train-violation').textContent = violation === null ? '-' : (violation ? 'YES ❌' : 'NO ✓');
}
function drawSVMTraining() {
const canvas = document.getElementById('svm-train-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 450;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
const xMin = 0, xMax = 10;
const yMin = 0, yMax = 10;
const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * chartWidth;
const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * chartHeight;
const w = state.svm.training.w;
const b = state.svm.training.b;
// Draw boundary if weights are non-zero
if (Math.abs(w[1]) > 0.01) {
ctx.strokeStyle = '#6aa9ff';
ctx.lineWidth = 3;
ctx.beginPath();
const x1 = xMin, y1 = -(w[0] * x1 + b) / w[1];
const x2 = xMax, y2 = -(w[0] * x2 + b) / w[1];
ctx.moveTo(scaleX(x1), scaleY(y1));
ctx.lineTo(scaleX(x2), scaleY(y2));
ctx.stroke();
}
// Draw points
data.svm.forEach((point, i) => {
const x = scaleX(point.x1);
const y = scaleY(point.x2);
const processed = i < state.svm.training.step;
const current = i === state.svm.training.step - 1;
ctx.fillStyle = point.class === 1 ? '#7ef0d4' : '#ff8c6a';
ctx.globalAlpha = processed ? 1 : 0.3;
ctx.beginPath();
ctx.arc(x, y, 8, 0, 2 * Math.PI);
ctx.fill();
if (current) {
ctx.globalAlpha = 1;
ctx.strokeStyle = '#ffff00';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(x, y, 14, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.globalAlpha = 1;
ctx.fillStyle = '#e8eef6';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(point.label, x, y - 15);
});
}
function initSVMKernel() {
const canvas = document.getElementById('svm-kernel-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
const kernelRadios = document.querySelectorAll('input[name="kernel"]');
kernelRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
state.svm.kernel = e.target.value;
const paramGroup = document.getElementById('kernel-param-group');
if (paramGroup) {
paramGroup.style.display = state.svm.kernel === 'linear' ? 'none' : 'block';
}
drawSVMKernel();
});
});
const paramSlider = document.getElementById('kernel-param-slider');
if (paramSlider) {
paramSlider.addEventListener('input', (e) => {
state.svm.kernelParam = parseFloat(e.target.value);
document.getElementById('kernel-param-val').textContent = state.svm.kernelParam.toFixed(1);
drawSVMKernel();
});
}
drawSVMKernel();
}
function drawSVMKernel() {
const canvas = document.getElementById('svm-kernel-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 500;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
// Generate circular data
const innerPoints = [];
const outerPoints = [];
for (let i = 0; i < 15; i++) {
const angle = (i / 15) * 2 * Math.PI;
innerPoints.push({ x: 5 + 1.5 * Math.cos(angle), y: 5 + 1.5 * Math.sin(angle), class: 1 });
}
for (let i = 0; i < 20; i++) {
const angle = (i / 20) * 2 * Math.PI;
const r = 3.5 + Math.random() * 0.5;
outerPoints.push({ x: 5 + r * Math.cos(angle), y: 5 + r * Math.sin(angle), class: -1 });
}
const allPoints = [...innerPoints, ...outerPoints];
const xMin = 0, xMax = 10;
const yMin = 0, yMax = 10;
const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * chartWidth;
const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * chartHeight;
// Draw decision boundary based on kernel
if (state.svm.kernel === 'linear') {
// Linear can't separate circular data well
ctx.strokeStyle = 'rgba(106, 169, 255, 0.5)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(scaleX(2), scaleY(2));
ctx.lineTo(scaleX(8), scaleY(8));
ctx.stroke();
} else if (state.svm.kernel === 'polynomial' || state.svm.kernel === 'rbf') {
// Draw circular boundary
ctx.strokeStyle = '#6aa9ff';
ctx.lineWidth = 3;
ctx.beginPath();
const radius = state.svm.kernel === 'polynomial' ? 2.5 : 2.3 + state.svm.kernelParam * 0.1;
ctx.arc(scaleX(5), scaleY(5), radius * (chartWidth / 10), 0, 2 * Math.PI);
ctx.stroke();
}
// Draw points
allPoints.forEach(point => {
const x = scaleX(point.x);
const y = scaleY(point.y);
ctx.fillStyle = point.class === 1 ? '#7ef0d4' : '#ff8c6a';
ctx.beginPath();
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fill();
});
// Draw kernel info
ctx.fillStyle = '#7ef0d4';
ctx.font = '16px sans-serif';
ctx.textAlign = 'left';
const kernelName = state.svm.kernel === 'linear' ? 'Linear Kernel' :
state.svm.kernel === 'polynomial' ? 'Polynomial Kernel' : 'RBF Kernel';
ctx.fillText(kernelName, padding + 10, padding + 25);
if (state.svm.kernel === 'linear') {
ctx.font = '13px sans-serif';
ctx.fillStyle = '#ff8c6a';
ctx.fillText('❌ Linear kernel cannot separate circular data!', padding + 10, padding + 50);
} else {
ctx.font = '13px sans-serif';
ctx.fillStyle = '#7ef0d4';
ctx.fillText('✓ Non-linear kernel successfully separates the data', padding + 10, padding + 50);
}
}
// Logistic Regression Visualizations
function initLogistic() {
initSigmoid();
initLogisticClassification();
}
function initSigmoid() {
const canvas = document.getElementById('sigmoid-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
drawSigmoid();
}
function drawSigmoid() {
const canvas = document.getElementById('sigmoid-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 350;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
const zMin = -10, zMax = 10;
const scaleX = (z) => padding + ((z - zMin) / (zMax - zMin)) * chartWidth;
const scaleY = (sig) => height - padding - sig * chartHeight;
// Draw grid
ctx.strokeStyle = 'rgba(42, 53, 68, 0.5)';
ctx.lineWidth = 1;
for (let i = 0; i <= 10; i++) {
const x = padding + (chartWidth / 10) * i;
ctx.beginPath();
ctx.moveTo(x, padding);
ctx.lineTo(x, height - padding);
ctx.stroke();
const y = padding + (chartHeight / 10) * i;
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
}
// Draw axes
ctx.strokeStyle = '#2a3544';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding);
ctx.stroke();
// Draw threshold line at 0.5
ctx.strokeStyle = '#ff8c6a';
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(padding, scaleY(0.5));
ctx.lineTo(width - padding, scaleY(0.5));
ctx.stroke();
ctx.setLineDash([]);
// Draw sigmoid curve
ctx.strokeStyle = '#7ef0d4';
ctx.lineWidth = 3;
ctx.beginPath();
for (let z = zMin; z <= zMax; z += 0.1) {
const sig = 1 / (1 + Math.exp(-z));
const x = scaleX(z);
const y = scaleY(sig);
if (z === zMin) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// Labels
ctx.fillStyle = '#a9b4c2';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('z (input)', width / 2, height - 20);
ctx.save();
ctx.translate(20, height / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('σ(z) probability', 0, 0);
ctx.restore();
// Annotations
ctx.fillStyle = '#7ef0d4';
ctx.textAlign = 'left';
ctx.fillText('σ(z) = 1/(1+e⁻ᶻ)', padding + 10, padding + 25);
ctx.fillStyle = '#ff8c6a';
ctx.fillText('Threshold = 0.5', padding + 10, scaleY(0.5) - 10);
}
function initLogisticClassification() {
const canvas = document.getElementById('logistic-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
drawLogisticClassification();
}
function drawLogisticClassification() {
const canvas = document.getElementById('logistic-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 400;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
const hMin = 140, hMax = 210;
const scaleX = (h) => padding + ((h - hMin) / (hMax - hMin)) * chartWidth;
const scaleY = (p) => height - padding - p * chartHeight;
// Draw sigmoid curve
ctx.strokeStyle = '#6aa9ff';
ctx.lineWidth = 3;
ctx.beginPath();
for (let h = hMin; h <= hMax; h += 1) {
const z = (h - 170) / 10; // Simple linear transformation
const p = 1 / (1 + Math.exp(-z));
const x = scaleX(h);
const y = scaleY(p);
if (h === hMin) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// Draw threshold line
ctx.strokeStyle = '#ff8c6a';
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(padding, scaleY(0.5));
ctx.lineTo(width - padding, scaleY(0.5));
ctx.stroke();
ctx.setLineDash([]);
// Draw data points
data.logistic.forEach(point => {
const x = scaleX(point.height);
const y = scaleY(point.prob);
ctx.fillStyle = point.label === 1 ? '#7ef0d4' : '#ff8c6a';
ctx.beginPath();
ctx.arc(x, y, 6, 0, 2 * Math.PI);
ctx.fill();
// Label
ctx.fillStyle = '#e8eef6';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(point.height, x, height - padding + 20);
});
// Labels
ctx.fillStyle = '#a9b4c2';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Height (cm)', width / 2, height - 20);
ctx.save();
ctx.translate(20, height / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('P(Tall)', 0, 0);
ctx.restore();
}
// KNN Visualization
let knnState = { testPoint: { x: 2.5, y: 2.5 }, k: 3, distanceMetric: 'euclidean', dragging: false };
function initKNN() {
const canvas = document.getElementById('knn-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
const kSlider = document.getElementById('knn-k-slider');
if (kSlider) {
kSlider.addEventListener('input', (e) => {
knnState.k = parseInt(e.target.value);
document.getElementById('knn-k-val').textContent = knnState.k;
drawKNN();
});
}
const distanceRadios = document.querySelectorAll('input[name="knn-distance"]');
distanceRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
knnState.distanceMetric = e.target.value;
drawKNN();
});
});
canvas.addEventListener('mousedown', startDragKNN);
canvas.addEventListener('mousemove', dragKNN);
canvas.addEventListener('mouseup', stopDragKNN);
drawKNN();
}
function startDragKNN(e) {
const canvas = document.getElementById('knn-canvas');
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const padding = 60;
const chartWidth = canvas.width - 2 * padding;
const chartHeight = canvas.height - 2 * padding;
const tx = padding + (knnState.testPoint.x / 6) * chartWidth;
const ty = canvas.height - padding - (knnState.testPoint.y / 6) * chartHeight;
if (Math.abs(mx - tx) < 15 && Math.abs(my - ty) < 15) {
knnState.dragging = true;
}
}
function dragKNN(e) {
if (!knnState.dragging) return;
const canvas = document.getElementById('knn-canvas');
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const padding = 60;
const chartWidth = canvas.width - 2 * padding;
const chartHeight = canvas.height - 2 * padding;
knnState.testPoint.x = Math.max(0, Math.min(6, ((mx - padding) / chartWidth) * 6));
knnState.testPoint.y = Math.max(0, Math.min(6, ((canvas.height - padding - my) / chartHeight) * 6));
drawKNN();
}
function stopDragKNN() {
knnState.dragging = false;
}
function drawKNN() {
const canvas = document.getElementById('knn-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 450;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
const scaleX = (x) => padding + (x / 6) * chartWidth;
const scaleY = (y) => height - padding - (y / 6) * chartHeight;
// Calculate distances
const distances = data.knn.map(point => {
let d;
if (knnState.distanceMetric === 'euclidean') {
d = Math.sqrt(Math.pow(point.x - knnState.testPoint.x, 2) + Math.pow(point.y - knnState.testPoint.y, 2));
} else {
d = Math.abs(point.x - knnState.testPoint.x) + Math.abs(point.y - knnState.testPoint.y);
}
return { ...point, distance: d };
});
distances.sort((a, b) => a.distance - b.distance);
const kNearest = distances.slice(0, knnState.k);
// Count votes
const votes = {};
kNearest.forEach(p => {
votes[p.class] = (votes[p.class] || 0) + 1;
});
const prediction = Object.keys(votes).reduce((a, b) => votes[a] > votes[b] ? a : b);
// Draw lines to K nearest
kNearest.forEach(point => {
ctx.strokeStyle = 'rgba(126, 240, 212, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(scaleX(knnState.testPoint.x), scaleY(knnState.testPoint.y));
ctx.lineTo(scaleX(point.x), scaleY(point.y));
ctx.stroke();
});
// Draw training points
distances.forEach(point => {
const x = scaleX(point.x);
const y = scaleY(point.y);
const isNearest = kNearest.includes(point);
ctx.fillStyle = point.class === 'orange' ? '#ff8c6a' : '#ffeb3b';
ctx.globalAlpha = isNearest ? 1 : 0.5;
ctx.beginPath();
ctx.arc(x, y, 8, 0, 2 * Math.PI);
ctx.fill();
if (isNearest) {
ctx.strokeStyle = '#7ef0d4';
ctx.lineWidth = 2;
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.arc(x, y, 12, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.globalAlpha = 1;
});
// Draw test point
const tx = scaleX(knnState.testPoint.x);
const ty = scaleY(knnState.testPoint.y);
ctx.fillStyle = prediction === 'orange' ? '#ff8c6a' : '#ffeb3b';
ctx.beginPath();
ctx.arc(tx, ty, 12, 0, 2 * Math.PI);
ctx.fill();
ctx.strokeStyle = '#6aa9ff';
ctx.lineWidth = 3;
ctx.stroke();
// Info
ctx.fillStyle = '#7ef0d4';
ctx.font = '14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`K=${knnState.k} | Prediction: ${prediction}`, padding + 10, padding + 25);
ctx.fillText(`Votes: Orange=${votes.orange || 0}, Yellow=${votes.yellow || 0}`, padding + 10, padding + 50);
}
// Model Evaluation
function initModelEvaluation() {
initConfusionMatrix();
initROC();
initR2();
}
function initConfusionMatrix() {
const canvas = document.getElementById('confusion-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
drawConfusionMatrix();
}
function drawConfusionMatrix() {
const canvas = document.getElementById('confusion-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 300;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const size = Math.min(width, height) - 100;
const cellSize = size / 2;
const startX = (width - size) / 2;
const startY = 50;
const cm = { tp: 600, fp: 100, fn: 300, tn: 900 };
// Draw cells
const cells = [
{ x: startX, y: startY, val: cm.tp, label: 'TP', color: '#7ef0d4' },
{ x: startX + cellSize, y: startY, val: cm.fn, label: 'FN', color: '#ff8c6a' },
{ x: startX, y: startY + cellSize, val: cm.fp, label: 'FP', color: '#ff8c6a' },
{ x: startX + cellSize, y: startY + cellSize, val: cm.tn, label: 'TN', color: '#7ef0d4' }
];
cells.forEach(cell => {
ctx.fillStyle = cell.color + '22';
ctx.fillRect(cell.x, cell.y, cellSize, cellSize);
ctx.strokeStyle = cell.color;
ctx.lineWidth = 2;
ctx.strokeRect(cell.x, cell.y, cellSize, cellSize);
ctx.fillStyle = cell.color;
ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(cell.label, cell.x + cellSize / 2, cell.y + cellSize / 2 - 10);
ctx.font = 'bold 32px sans-serif';
ctx.fillText(cell.val, cell.x + cellSize / 2, cell.y + cellSize / 2 + 25);
});
// Labels
ctx.fillStyle = '#a9b4c2';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Predicted Positive', startX + cellSize / 2, startY - 15);
ctx.fillText('Predicted Negative', startX + cellSize * 1.5, startY - 15);
ctx.save();
ctx.translate(startX - 30, startY + cellSize / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Actual Positive', 0, 0);
ctx.restore();
ctx.save();
ctx.translate(startX - 30, startY + cellSize * 1.5);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Actual Negative', 0, 0);
ctx.restore();
}
let rocState = { threshold: 0.5 };
function initROC() {
const canvas = document.getElementById('roc-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
const slider = document.getElementById('roc-threshold-slider');
if (slider) {
slider.addEventListener('input', (e) => {
rocState.threshold = parseFloat(e.target.value);
document.getElementById('roc-threshold-val').textContent = rocState.threshold.toFixed(1);
drawROC();
});
}
drawROC();
}
function drawROC() {
const canvas = document.getElementById('roc-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 450;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const padding = 60;
const chartSize = Math.min(width - 2 * padding, height - 2 * padding);
const chartX = (width - chartSize) / 2;
const chartY = (height - chartSize) / 2;
// Calculate ROC points
const rocPoints = [];
for (let t = 0; t <= 1; t += 0.1) {
let tp = 0, fp = 0, tn = 0, fn = 0;
data.roc.forEach(e => {
const pred = e.score >= t ? 1 : 0;
if (e.true_label === 1 && pred === 1) tp++;
else if (e.true_label === 0 && pred === 1) fp++;
else if (e.true_label === 1 && pred === 0) fn++;
else tn++;
});
const tpr = tp / (tp + fn) || 0;
const fpr = fp / (fp + tn) || 0;
rocPoints.push({ t, tpr, fpr });
}
// Current threshold point
let tp = 0, fp = 0, tn = 0, fn = 0;
data.roc.forEach(e => {
const pred = e.score >= rocState.threshold ? 1 : 0;
if (e.true_label === 1 && pred === 1) tp++;
else if (e.true_label === 0 && pred === 1) fp++;
else if (e.true_label === 1 && pred === 0) fn++;
else tn++;
});
const tpr = tp / (tp + fn) || 0;
const fpr = fp / (fp + tn) || 0;
// Draw diagonal (random)
ctx.strokeStyle = 'rgba(255, 140, 106, 0.5)';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(chartX, chartY + chartSize);
ctx.lineTo(chartX + chartSize, chartY);
ctx.stroke();
ctx.setLineDash([]);
// Draw ROC curve
ctx.strokeStyle = '#6aa9ff';
ctx.lineWidth = 3;
ctx.beginPath();
rocPoints.forEach((p, i) => {
const x = chartX + p.fpr * chartSize;
const y = chartY + chartSize - p.tpr * chartSize;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
// Draw current point
const cx = chartX + fpr * chartSize;
const cy = chartY + chartSize - tpr * chartSize;
ctx.fillStyle = '#7ef0d4';
ctx.beginPath();
ctx.arc(cx, cy, 8, 0, 2 * Math.PI);
ctx.fill();
// Draw axes
ctx.strokeStyle = '#2a3544';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.rect(chartX, chartY, chartSize, chartSize);
ctx.stroke();
// Labels
ctx.fillStyle = '#a9b4c2';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('FPR (False Positive Rate)', width / 2, height - 20);
ctx.save();
ctx.translate(20, height / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('TPR (True Positive Rate)', 0, 0);
ctx.restore();
// Info
ctx.fillStyle = '#7ef0d4';
ctx.font = '14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`TPR: ${tpr.toFixed(2)} | FPR: ${fpr.toFixed(2)}`, chartX + 10, chartY + 25);
ctx.fillText(`TP=${tp} FP=${fp} TN=${tn} FN=${fn}`, chartX + 10, chartY + 50);
}
function initR2() {
const canvas = document.getElementById('r2-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
drawR2();
}
function drawR2() {
const canvas = document.getElementById('r2-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 350;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
// Dummy R² data
const r2data = [
{ x: 150, y: 50, pred: 52 },
{ x: 160, y: 60, pred: 61 },
{ x: 170, y: 70, pred: 69 },
{ x: 180, y: 80, pred: 78 },
{ x: 190, y: 90, pred: 87 }
];
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
const xMin = 140, xMax = 200, yMin = 40, yMax = 100;
const scaleX = (x) => padding + ((x - xMin) / (xMax - xMin)) * chartWidth;
const scaleY = (y) => height - padding - ((y - yMin) / (yMax - yMin)) * chartHeight;
// Mean
const mean = r2data.reduce((sum, p) => sum + p.y, 0) / r2data.length;
// Draw mean line
ctx.strokeStyle = '#ff8c6a';
ctx.setLineDash([5, 5]);
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding, scaleY(mean));
ctx.lineTo(width - padding, scaleY(mean));
ctx.stroke();
ctx.setLineDash([]);
// Draw regression line
ctx.strokeStyle = '#6aa9ff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(scaleX(xMin), scaleY(40));
ctx.lineTo(scaleX(xMax), scaleY(95));
ctx.stroke();
// Draw points
r2data.forEach(p => {
// Residual line
ctx.strokeStyle = 'rgba(126, 240, 212, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(scaleX(p.x), scaleY(p.y));
ctx.lineTo(scaleX(p.x), scaleY(p.pred));
ctx.stroke();
// Actual point
ctx.fillStyle = '#7ef0d4';
ctx.beginPath();
ctx.arc(scaleX(p.x), scaleY(p.y), 6, 0, 2 * Math.PI);
ctx.fill();
});
// Calculate R²
let ssRes = 0, ssTot = 0;
r2data.forEach(p => {
ssRes += Math.pow(p.y - p.pred, 2);
ssTot += Math.pow(p.y - mean, 2);
});
const r2 = 1 - (ssRes / ssTot);
// Info
ctx.fillStyle = '#7ef0d4';
ctx.font = '16px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`R² = ${r2.toFixed(3)}`, padding + 10, padding + 25);
ctx.fillText(`Model explains ${(r2 * 100).toFixed(1)}% of variance`, padding + 10, padding + 50);
}
// Regularization
let regState = { lambda: 0.1 };
function initRegularization() {
const canvas = document.getElementById('regularization-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
const slider = document.getElementById('reg-lambda-slider');
if (slider) {
slider.addEventListener('input', (e) => {
regState.lambda = parseFloat(e.target.value);
document.getElementById('reg-lambda-val').textContent = regState.lambda.toFixed(1);
drawRegularization();
});
}
drawRegularization();
}
function drawRegularization() {
const canvas = document.getElementById('regularization-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 400;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
const features = ['x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9', 'x10'];
const vanilla = [100, 200, 300, 50, 150, 250, 80, 120, 90, 180];
// Simulate L1 and L2 effects
const l1 = vanilla.map(v => Math.abs(v) > 50 / regState.lambda ? v * (1 - regState.lambda * 0.5) : 0);
const l2 = vanilla.map(v => v / (1 + regState.lambda));
const barWidth = chartWidth / (features.length * 3.5);
const maxVal = Math.max(...vanilla);
features.forEach((f, i) => {
const x = padding + (i * chartWidth / features.length);
// Vanilla
const h1 = (vanilla[i] / maxVal) * chartHeight * 0.8;
ctx.fillStyle = '#a9b4c2';
ctx.fillRect(x, height - padding - h1, barWidth, h1);
// L1
const h2 = (l1[i] / maxVal) * chartHeight * 0.8;
ctx.fillStyle = '#ff8c6a';
ctx.fillRect(x + barWidth * 1.2, height - padding - h2, barWidth, h2);
// L2
const h3 = (l2[i] / maxVal) * chartHeight * 0.8;
ctx.fillStyle = '#6aa9ff';
ctx.fillRect(x + barWidth * 2.4, height - padding - h3, barWidth, h3);
// Feature label
ctx.fillStyle = '#a9b4c2';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(f, x + barWidth * 1.5, height - padding + 20);
});
// Legend
const legendY = padding + 20;
ctx.fillStyle = '#a9b4c2';
ctx.fillRect(padding + 10, legendY, 15, 15);
ctx.fillStyle = '#e8eef6';
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('Vanilla', padding + 30, legendY + 12);
ctx.fillStyle = '#ff8c6a';
ctx.fillRect(padding + 100, legendY, 15, 15);
ctx.fillStyle = '#e8eef6';
ctx.fillText('L1 (Lasso)', padding + 120, legendY + 12);
ctx.fillStyle = '#6aa9ff';
ctx.fillRect(padding + 210, legendY, 15, 15);
ctx.fillStyle = '#e8eef6';
ctx.fillText('L2 (Ridge)', padding + 230, legendY + 12);
}
// Bias-Variance
function initBiasVariance() {
const canvas = document.getElementById('bias-variance-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
drawBiasVariance();
const canvas2 = document.getElementById('complexity-canvas');
if (canvas2 && !canvas2.dataset.initialized) {
canvas2.dataset.initialized = 'true';
drawComplexityCurve();
}
}
function drawBiasVariance() {
const canvas = document.getElementById('bias-variance-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 400;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const sectionWidth = width / 3;
const padding = 40;
const chartHeight = height - 2 * padding;
// Generate curved data
const trueData = [];
for (let x = 0; x <= 10; x += 0.5) {
trueData.push({ x, y: 50 + 30 * Math.sin(x / 2) });
}
// Draw three scenarios
const scenarios = [
{ title: 'High Bias\n(Underfit)', color: '#ff8c6a', degree: 1 },
{ title: 'Good Fit', color: '#7ef0d4', degree: 2 },
{ title: 'High Variance\n(Overfit)', color: '#ff8c6a', degree: 8 }
];
scenarios.forEach((scenario, idx) => {
const offsetX = idx * sectionWidth;
const scaleX = (x) => offsetX + padding + (x / 10) * (sectionWidth - 2 * padding);
const scaleY = (y) => padding + chartHeight - ((y - 20) / 80) * chartHeight;
// Draw true curve
ctx.strokeStyle = 'rgba(106, 169, 255, 0.3)';
ctx.lineWidth = 2;
ctx.beginPath();
trueData.forEach((p, i) => {
if (i === 0) ctx.moveTo(scaleX(p.x), scaleY(p.y));
else ctx.lineTo(scaleX(p.x), scaleY(p.y));
});
ctx.stroke();
// Draw model fit
ctx.strokeStyle = scenario.color;
ctx.lineWidth = 3;
ctx.beginPath();
if (scenario.degree === 1) {
// Straight line
ctx.moveTo(scaleX(0), scaleY(50));
ctx.lineTo(scaleX(10), scaleY(65));
} else if (scenario.degree === 2) {
// Good fit
trueData.forEach((p, i) => {
const noise = (Math.random() - 0.5) * 3;
if (i === 0) ctx.moveTo(scaleX(p.x), scaleY(p.y + noise));
else ctx.lineTo(scaleX(p.x), scaleY(p.y + noise));
});
} else {
// Wiggly overfit
for (let x = 0; x <= 10; x += 0.2) {
const y = 50 + 30 * Math.sin(x / 2) + 15 * Math.sin(x * 2);
if (x === 0) ctx.moveTo(scaleX(x), scaleY(y));
else ctx.lineTo(scaleX(x), scaleY(y));
}
}
ctx.stroke();
// Title
ctx.fillStyle = scenario.color;
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
const lines = scenario.title.split('\n');
lines.forEach((line, i) => {
ctx.fillText(line, offsetX + sectionWidth / 2, 20 + i * 18);
});
});
}
function drawComplexityCurve() {
const canvas = document.getElementById('complexity-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 350;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
const scaleX = (x) => padding + (x / 10) * chartWidth;
const scaleY = (y) => padding + chartHeight - (y / 100) * chartHeight;
// Draw curves
ctx.strokeStyle = '#ff8c6a';
ctx.lineWidth = 3;
ctx.beginPath();
for (let x = 0; x <= 10; x += 0.1) {
const trainError = 80 * Math.exp(-x / 2) + 5;
if (x === 0) ctx.moveTo(scaleX(x), scaleY(trainError));
else ctx.lineTo(scaleX(x), scaleY(trainError));
}
ctx.stroke();
ctx.strokeStyle = '#6aa9ff';
ctx.beginPath();
for (let x = 0; x <= 10; x += 0.1) {
const testError = 80 * Math.exp(-x / 2) + 5 + 15 * (x / 10) ** 2;
if (x === 0) ctx.moveTo(scaleX(x), scaleY(testError));
else ctx.lineTo(scaleX(x), scaleY(testError));
}
ctx.stroke();
// Sweet spot
ctx.fillStyle = '#7ef0d4';
ctx.beginPath();
ctx.arc(scaleX(5), scaleY(18), 8, 0, 2 * Math.PI);
ctx.fill();
// Legend
ctx.fillStyle = '#ff8c6a';
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('Training Error', padding + 10, padding + 20);
ctx.fillStyle = '#6aa9ff';
ctx.fillText('Test Error', padding + 10, padding + 40);
ctx.fillStyle = '#7ef0d4';
ctx.fillText('● Sweet Spot', padding + 10, padding + 60);
// Labels
ctx.fillStyle = '#a9b4c2';
ctx.textAlign = 'center';
ctx.fillText('Model Complexity →', width / 2, height - 20);
ctx.save();
ctx.translate(20, height / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Error', 0, 0);
ctx.restore();
}
// Cross-Validation
function initCrossValidation() {
const canvas = document.getElementById('cv-canvas');
if (!canvas || canvas.dataset.initialized) return;
canvas.dataset.initialized = 'true';
drawCrossValidation();
}
function drawCrossValidation() {
const canvas = document.getElementById('cv-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 400;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const blockSize = 50;
const gap = 10;
const numBlocks = 12;
const k = 3;
const blocksPerFold = numBlocks / k;
const startX = (width - (numBlocks * blockSize + (numBlocks - 1) * gap)) / 2;
const folds = [0.96, 0.84, 0.90];
for (let fold = 0; fold < k; fold++) {
const offsetY = 80 + fold * 120;
// Fold label
ctx.fillStyle = '#e8eef6';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'right';
ctx.fillText(`Fold ${fold + 1}:`, startX - 20, offsetY + blockSize / 2 + 5);
// Draw blocks
for (let i = 0; i < numBlocks; i++) {
const x = startX + i * (blockSize + gap);
const isFold = i >= fold * blocksPerFold && i < (fold + 1) * blocksPerFold;
ctx.fillStyle = isFold ? '#6aa9ff' : '#7ef0d4';
ctx.fillRect(x, offsetY, blockSize, blockSize);
// Label
ctx.fillStyle = '#1a2332';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(String.fromCharCode(65 + i), x + blockSize / 2, offsetY + blockSize / 2 + 5);
}
// Accuracy
ctx.fillStyle = '#7ef0d4';
ctx.font = '14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`Acc: ${folds[fold].toFixed(2)}`, startX + numBlocks * (blockSize + gap) + 20, offsetY + blockSize / 2 + 5);
}
// Legend
ctx.fillStyle = '#6aa9ff';
ctx.fillRect(startX, 30, 30, 20);
ctx.fillStyle = '#e8eef6';
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('Test Set', startX + 40, 45);
ctx.fillStyle = '#7ef0d4';
ctx.fillRect(startX + 120, 30, 30, 20);
ctx.fillText('Training Set', startX + 160, 45);
// Final result
const mean = folds.reduce((a, b) => a + b) / folds.length;
const std = Math.sqrt(folds.reduce((sum, x) => sum + Math.pow(x - mean, 2), 0) / folds.length);
ctx.fillStyle = '#7ef0d4';
ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`Final Score: ${mean.toFixed(2)} ± ${std.toFixed(3)}`, width / 2, height - 20);
}
// Preprocessing
function initPreprocessing() {
const canvas = document.getElementById('scaling-canvas');
if (canvas && !canvas.dataset.initialized) {
canvas.dataset.initialized = 'true';
drawScaling();
}
const canvas2 = document.getElementById('pipeline-canvas');
if (canvas2 && !canvas2.dataset.initialized) {
canvas2.dataset.initialized = 'true';
drawPipeline();
}
}
function drawScaling() {
const canvas = document.getElementById('scaling-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 350;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const before = [10, 20, 30, 40, 50];
const standard = [-1.26, -0.63, 0, 0.63, 1.26];
const minmax = [0, 0.25, 0.5, 0.75, 1.0];
const sectionWidth = width / 3;
const padding = 40;
const barWidth = 30;
const datasets = [
{ data: before, title: 'Original', maxVal: 60 },
{ data: standard, title: 'StandardScaler', maxVal: 2 },
{ data: minmax, title: 'MinMaxScaler', maxVal: 1.2 }
];
datasets.forEach((dataset, idx) => {
const offsetX = idx * sectionWidth;
const centerX = offsetX + sectionWidth / 2;
// Title
ctx.fillStyle = '#7ef0d4';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(dataset.title, centerX, 30);
// Draw bars
dataset.data.forEach((val, i) => {
const barHeight = Math.abs(val) / dataset.maxVal * 200;
const x = centerX - barWidth / 2;
const y = val >= 0 ? 200 - barHeight : 200;
ctx.fillStyle = '#6aa9ff';
ctx.fillRect(x, y, barWidth, barHeight);
// Value label
ctx.fillStyle = '#a9b4c2';
ctx.font = '10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(val.toFixed(2), centerX, val >= 0 ? y - 5 : y + barHeight + 15);
centerX += 35;
});
});
}
function drawPipeline() {
const canvas = document.getElementById('pipeline-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 300;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const steps = ['Raw Data', 'Handle Missing', 'Encode Categories', 'Scale Features', 'Train Model'];
const stepWidth = (width - 100) / steps.length;
const y = height / 2;
steps.forEach((step, i) => {
const x = 50 + i * stepWidth;
// Box
ctx.fillStyle = '#2a3544';
ctx.fillRect(x, y - 30, stepWidth - 40, 60);
ctx.strokeStyle = '#6aa9ff';
ctx.lineWidth = 2;
ctx.strokeRect(x, y - 30, stepWidth - 40, 60);
// Text
ctx.fillStyle = '#e8eef6';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
const words = step.split(' ');
words.forEach((word, j) => {
ctx.fillText(word, x + (stepWidth - 40) / 2, y + j * 15 - 5);
});
// Arrow
if (i < steps.length - 1) {
ctx.strokeStyle = '#7ef0d4';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x + stepWidth - 40, y);
ctx.lineTo(x + stepWidth - 10, y);
ctx.stroke();
// Arrowhead
ctx.fillStyle = '#7ef0d4';
ctx.beginPath();
ctx.moveTo(x + stepWidth - 10, y);
ctx.lineTo(x + stepWidth - 20, y - 5);
ctx.lineTo(x + stepWidth - 20, y + 5);
ctx.fill();
}
});
}
// Loss Functions
function initLossFunctions() {
const canvas = document.getElementById('loss-comparison-canvas');
if (canvas && !canvas.dataset.initialized) {
canvas.dataset.initialized = 'true';
drawLossComparison();
}
const canvas2 = document.getElementById('loss-curves-canvas');
if (canvas2 && !canvas2.dataset.initialized) {
canvas2.dataset.initialized = 'true';
drawLossCurves();
}
}
function drawLossComparison() {
const canvas = document.getElementById('loss-comparison-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 400;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const actual = [10, 20, 30, 40, 50];
const predicted = [12, 19, 32, 38, 51];
// Calculate losses
let mse = 0, mae = 0;
actual.forEach((a, i) => {
const error = a - predicted[i];
mse += error * error;
mae += Math.abs(error);
});
mse /= actual.length;
mae /= actual.length;
const rmse = Math.sqrt(mse);
// Display
const padding = 60;
const barHeight = 60;
const startY = 100;
const maxWidth = width - 2 * padding;
const losses = [
{ name: 'MSE', value: mse, color: '#ff8c6a' },
{ name: 'MAE', value: mae, color: '#6aa9ff' },
{ name: 'RMSE', value: rmse, color: '#7ef0d4' }
];
const maxLoss = Math.max(...losses.map(l => l.value));
losses.forEach((loss, i) => {
const y = startY + i * (barHeight + 30);
const barWidth = (loss.value / maxLoss) * maxWidth;
// Bar
ctx.fillStyle = loss.color;
ctx.fillRect(padding, y, barWidth, barHeight);
// Label
ctx.fillStyle = '#e8eef6';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(loss.name, padding + 10, y + barHeight / 2 + 5);
// Value
ctx.font = '16px sans-serif';
ctx.textAlign = 'right';
ctx.fillText(loss.value.toFixed(2), padding + barWidth - 10, y + barHeight / 2 + 5);
});
// Title
ctx.fillStyle = '#7ef0d4';
ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Regression Loss Comparison', width / 2, 50);
}
function drawLossCurves() {
const canvas = document.getElementById('loss-curves-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width = canvas.offsetWidth;
const height = canvas.height = 350;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#1a2332';
ctx.fillRect(0, 0, width, height);
const padding = 60;
const chartWidth = width - 2 * padding;
const chartHeight = height - 2 * padding;
const scaleX = (x) => padding + (x / 10) * chartWidth;
const scaleY = (y) => height - padding - (y / 100) * chartHeight;
// Draw MSE curve
ctx.strokeStyle = '#ff8c6a';
ctx.lineWidth = 3;
ctx.beginPath();
for (let x = -10; x <= 10; x += 0.2) {
const y = x * x;
if (x === -10) ctx.moveTo(scaleX(x + 10), scaleY(y));
else ctx.lineTo(scaleX(x + 10), scaleY(y));
}
ctx.stroke();
// Draw MAE curve
ctx.strokeStyle = '#6aa9ff';
ctx.lineWidth = 3;
ctx.beginPath();
for (let x = -10; x <= 10; x += 0.2) {
const y = Math.abs(x) * 10;
if (x === -10) ctx.moveTo(scaleX(x + 10), scaleY(y));
else ctx.lineTo(scaleX(x + 10), scaleY(y));
}
ctx.stroke();
// Legend
ctx.fillStyle = '#ff8c6a';
ctx.font = '12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('MSE (quadratic penalty)', padding + 10, padding + 20);
ctx.fillStyle = '#6aa9ff';
ctx.fillText('MAE (linear penalty)', padding + 10, padding + 40);
// Labels
ctx.fillStyle = '#a9b4c2';
ctx.textAlign = 'center';
ctx.fillText('Error', width / 2, height - 20);
ctx.save();
ctx.translate(20, height / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Loss', 0, 0);
ctx.restore();
}
// Handle window resize
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
drawLinearRegression();
drawGradientDescent();
drawSigmoid();
drawLogisticClassification();
drawKNN();
drawConfusionMatrix();
drawROC();
drawR2();
drawRegularization();
drawBiasVariance();
drawComplexityCurve();
drawCrossValidation();
drawScaling();
drawPipeline();
drawLossComparison();
drawLossCurves();
drawSVMBasic();
drawSVMMargin();
drawSVMCParameter();
drawSVMTraining();
drawSVMKernel();
}, 250);
});