Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PCA Interactive Playground</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background-color: #f9fafb; | |
| color: #374151; | |
| line-height: 1.6; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 24px; | |
| background-color: white; | |
| } | |
| h1 { | |
| text-align: center; | |
| color: #1e40af; | |
| font-size: 2rem; | |
| font-weight: bold; | |
| margin-bottom: 24px; | |
| } | |
| .controls { | |
| background-color: #f3f4f6; | |
| padding: 20px; | |
| border-radius: 8px; | |
| margin-bottom: 24px; | |
| } | |
| .control-row { | |
| display: flex; | |
| gap: 16px; | |
| margin-bottom: 16px; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| } | |
| .control-section { | |
| margin-bottom: 16px; | |
| } | |
| .control-section h3 { | |
| font-size: 1.125rem; | |
| font-weight: 600; | |
| margin-bottom: 12px; | |
| } | |
| .dataset-buttons { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| margin-bottom: 12px; | |
| } | |
| .btn { | |
| padding: 8px 16px; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 0.875rem; | |
| transition: all 0.2s; | |
| font-weight: 500; | |
| } | |
| .btn-dataset { | |
| background-color: #dbeafe; | |
| color: #1d4ed8; | |
| } | |
| .btn-dataset.active { | |
| background-color: #2563eb; | |
| color: white; | |
| } | |
| .btn-dataset:hover { | |
| background-color: #bfdbfe; | |
| transform: translateY(-1px); | |
| } | |
| .btn-dataset.active:hover { | |
| background-color: #1d4ed8; | |
| } | |
| .btn-green { | |
| background-color: #10b981; | |
| color: white; | |
| } | |
| .btn-green:hover { | |
| background-color: #059669; | |
| transform: translateY(-1px); | |
| } | |
| .btn-blue { | |
| background-color: #3b82f6; | |
| color: white; | |
| } | |
| .btn-blue:hover { | |
| background-color: #2563eb; | |
| transform: translateY(-1px); | |
| } | |
| .btn-red { | |
| background-color: #ef4444; | |
| color: white; | |
| } | |
| .btn-red:hover { | |
| background-color: #dc2626; | |
| transform: translateY(-1px); | |
| } | |
| .noise-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| background-color: white; | |
| padding: 12px; | |
| border-radius: 6px; | |
| border: 2px solid #e5e7eb; | |
| } | |
| .noise-slider { | |
| width: 200px; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: linear-gradient(to right, #10b981, #f59e0b, #ef4444); | |
| outline: none; | |
| appearance: none; | |
| } | |
| .noise-slider::-webkit-slider-thumb { | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #3b82f6; | |
| cursor: pointer; | |
| border: 2px solid white; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.2); | |
| } | |
| .noise-slider::-moz-range-thumb { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #3b82f6; | |
| cursor: pointer; | |
| border: 2px solid white; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.2); | |
| } | |
| .step-section { | |
| border: 2px solid #d1d5db; | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-bottom: 24px; | |
| background-color: #fafafa; | |
| } | |
| .step-title { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| margin-bottom: 16px; | |
| color: #1f2937; | |
| } | |
| .grid-2 { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| } | |
| .chart-container { | |
| height: 350px; | |
| position: relative; | |
| background-color: white; | |
| border: 2px solid #e5e7eb; | |
| border-radius: 8px; | |
| padding: 12px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.05); | |
| } | |
| .data-display { | |
| background-color: #f8fafc; | |
| border: 1px solid #e2e8f0; | |
| padding: 12px; | |
| border-radius: 6px; | |
| font-family: 'Courier New', monospace; | |
| font-size: 0.8rem; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| line-height: 1.4; | |
| } | |
| .math-box { | |
| background-color: #eff6ff; | |
| padding: 16px; | |
| border-radius: 6px; | |
| font-size: 0.875rem; | |
| border: 1px solid #bfdbfe; | |
| } | |
| .concept-box { | |
| background-color: #dbeafe; | |
| border: 2px solid #93c5fd; | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-bottom: 24px; | |
| } | |
| .concept-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 16px; | |
| } | |
| .concept-card { | |
| background-color: white; | |
| border: 1px solid #e5e7eb; | |
| border-radius: 8px; | |
| padding: 16px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| } | |
| .concept-title { | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| font-size: 1rem; | |
| } | |
| .green-title { color: #059669; } | |
| .purple-title { color: #7c3aed; } | |
| .red-title { color: #dc2626; } | |
| .blue-title { color: #2563eb; } | |
| .yellow-title { color: #d97706; } | |
| .info-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 16px; | |
| } | |
| .info-card { | |
| background-color: white; | |
| border: 1px solid #e5e7eb; | |
| border-radius: 8px; | |
| padding: 16px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| } | |
| .stats-display { | |
| background-color: #f8fafc; | |
| padding: 12px; | |
| border-radius: 6px; | |
| border: 1px solid #e2e8f0; | |
| margin: 8px 0; | |
| } | |
| @media (max-width: 1024px) { | |
| .grid-2 { | |
| grid-template-columns: 1fr; | |
| } | |
| .concept-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .control-row { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .dataset-buttons { | |
| justify-content: center; | |
| } | |
| .noise-control { | |
| justify-content: center; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>PCA Interactive Playground</h1> | |
| <!-- Controls --> | |
| <div class="controls"> | |
| <div class="control-section"> | |
| <h3>Dataset Controls</h3> | |
| <div class="control-row"> | |
| <div class="dataset-buttons" id="datasetButtons"> | |
| <!-- Dataset buttons will be inserted here --> | |
| </div> | |
| </div> | |
| <div class="control-row"> | |
| <button class="btn btn-green" onclick="createNewRandomDataset()">🎲 Generate Random Dataset</button> | |
| <button class="btn btn-blue" onclick="addRandomPoint()">➕ Add Point</button> | |
| <button class="btn btn-red" onclick="removeLastPoint()">➖ Remove Point</button> | |
| </div> | |
| </div> | |
| <div class="control-section"> | |
| <h3>Noise Control</h3> | |
| <div class="noise-control"> | |
| <label for="noiseSlider" style="font-weight: 500; min-width: 80px;">Noise Level:</label> | |
| <input type="range" id="noiseSlider" class="noise-slider" min="0" max="100" value="0" onchange="updateNoise()"> | |
| <span id="noiseValue" style="font-weight: 600; min-width: 60px; color: #10b981;">0%</span> | |
| <button class="btn btn-blue" onclick="applyNoise()" style="margin-left: 8px;">Apply Noise</button> | |
| </div> | |
| </div> | |
| <div style="font-size: 0.875rem; color: #6b7280; text-align: center; margin-top: 12px;"> | |
| Current dataset: <strong id="pointCount">8 points</strong> | Selected: <strong id="selectedDataset">DEFAULT</strong> | Noise: <strong id="currentNoise">0%</strong> | |
| </div> | |
| </div> | |
| <!-- Key Concepts --> | |
| <div class="concept-box"> | |
| <h3 class="blue-title" style="font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;">Key Concepts</h3> | |
| <div class="concept-grid"> | |
| <div class="concept-card"> | |
| <h4 class="concept-title green-title">Augmented Vector</h4> | |
| <p style="font-size: 0.875rem;">An augmented vector is simply a vector with additional components. In our context, when we represent 2D data points as [x, y], we're using 2D vectors. The "augmented" aspect comes when we stack all data points into a matrix for processing.</p> | |
| </div> | |
| <div class="concept-card"> | |
| <h4 class="concept-title purple-title">Eigenvector & Eigenvalue</h4> | |
| <p style="font-size: 0.875rem;">For a matrix A, if Av = λv (where v ≠ 0), then v is an eigenvector and λ is its eigenvalue. Eigenvectors show directions of maximum variance in data, while eigenvalues show how much variance exists in those directions.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 1: Original Data --> | |
| <div class="step-section"> | |
| <h3 class="step-title">Step 1: Original Data Points</h3> | |
| <div class="grid-2"> | |
| <div class="chart-container"> | |
| <canvas id="originalChart"></canvas> | |
| </div> | |
| <div> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">Data Matrix (Augmented Vectors):</h4> | |
| <div class="data-display" id="originalData"></div> | |
| <div class="stats-display"> | |
| <p><strong>Mean:</strong> <span id="meanDisplay">[0.00, 0.00]</span></p> | |
| <p><strong>Data Points:</strong> <span id="dataPointCount">8</span></p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 2: Centered Data --> | |
| <div class="step-section"> | |
| <h3 class="step-title">Step 2: Centered Data (Mean Subtraction)</h3> | |
| <div class="grid-2"> | |
| <div class="chart-container"> | |
| <canvas id="centeredChart"></canvas> | |
| </div> | |
| <div> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">Centered Data:</h4> | |
| <div class="data-display" id="centeredData"></div> | |
| <div class="stats-display"> | |
| <p style="font-size: 0.875rem;"><strong>Process:</strong> Each point - mean = centered point</p> | |
| <p style="font-size: 0.875rem;">This centers the data around the origin (0,0)</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 3: Covariance Matrix --> | |
| <div class="step-section"> | |
| <h3 class="step-title">Step 3: Covariance Matrix</h3> | |
| <div class="grid-2"> | |
| <div> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">Covariance Matrix:</h4> | |
| <div class="data-display" id="covarianceMatrix"></div> | |
| <div class="stats-display"> | |
| <h5 style="font-weight: 600; margin-bottom: 4px;">Interpretation:</h5> | |
| <ul style="list-style-type: disc; padding-left: 20px; font-size: 0.875rem;"> | |
| <li id="varianceExplanation">Diagonal: Variance in X and Y</li> | |
| <li id="covarianceExplanation">Off-diagonal: Covariance between X and Y</li> | |
| <li>Positive covariance = variables tend to increase together</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">Formula:</h4> | |
| <div class="math-box"> | |
| <p><strong>Cov(X,Y) = Σ(xi - x̄)(yi - ȳ) / (n-1)</strong></p> | |
| <p style="margin-top: 8px;">Where:</p> | |
| <ul style="list-style-type: disc; padding-left: 20px; margin-top: 4px;"> | |
| <li>xi, yi are data points</li> | |
| <li>x̄, ȳ are means</li> | |
| <li>n is number of points</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 4: Eigenvectors and Eigenvalues --> | |
| <div class="step-section"> | |
| <h3 class="step-title">Step 4: Principal Components (Eigenvectors & Eigenvalues)</h3> | |
| <div class="grid-2"> | |
| <div style="position: relative;"> | |
| <svg id="eigenvectorSVG" width="100%" height="350" viewBox="-4 -4 8 8" style="border: 2px solid #e5e7eb; background-color: white; border-radius: 8px;"> | |
| <!-- Grid pattern --> | |
| <defs> | |
| <pattern id="grid" width="1" height="1" patternUnits="userSpaceOnUse"> | |
| <path d="M 1 0 L 0 0 0 1" fill="none" stroke="#e0e0e0" stroke-width="0.05"/> | |
| </pattern> | |
| </defs> | |
| <rect x="-4" y="-4" width="8" height="8" fill="url(#grid)" /> | |
| <!-- Axes --> | |
| <line x1="-4" y1="0" x2="4" y2="0" stroke="#9ca3af" stroke-width="0.08"/> | |
| <line x1="0" y1="-4" x2="0" y2="4" stroke="#9ca3af" stroke-width="0.08"/> | |
| </svg> | |
| <div style="text-align: center; font-size: 0.875rem; margin-top: 8px; font-weight: 500;"> | |
| <span style="color: #ef4444;">🔴 PC1 (Maximum Variance)</span> | | |
| <span style="color: #3b82f6;">🔵 PC2 (Orthogonal)</span> | |
| </div> | |
| </div> | |
| <div style="font-size: 0.875rem;"> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">Eigenvalues (Variance along each PC):</h4> | |
| <div id="eigenvalue1Info" style="background-color: #fef2f2; border: 2px solid #fecaca; padding: 12px; border-radius: 6px; margin-bottom: 8px;"> | |
| <div style="font-family: 'Courier New', monospace; font-size: 0.875rem;"> | |
| <div><strong>λ1 = <span id="lambda1">0.000000</span></strong> (PC1)</div> | |
| <div style="color: #dc2626;">Variance explained: <span id="variance1">0.00</span>%</div> | |
| </div> | |
| </div> | |
| <div id="eigenvalue2Info" style="background-color: #eff6ff; border: 2px solid #bfdbfe; padding: 12px; border-radius: 6px; margin-bottom: 12px;"> | |
| <div style="font-family: 'Courier New', monospace; font-size: 0.875rem;"> | |
| <div><strong>λ2 = <span id="lambda2">0.000000</span></strong> (PC2)</div> | |
| <div style="color: #2563eb;">Variance explained: <span id="variance2">0.00</span>%</div> | |
| </div> | |
| </div> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">Eigenvectors (PC Directions):</h4> | |
| <div id="eigenvector1Info" style="background-color: #fef2f2; border: 2px solid #fecaca; padding: 12px; border-radius: 6px; margin-bottom: 8px;"> | |
| <div style="font-family: 'Courier New', monospace; font-size: 0.875rem;"> | |
| <div><strong>PC1: [<span id="pc1Vector">0.000000, 0.000000</span>]</strong></div> | |
| <div style="color: #dc2626;">Angle: <span id="pc1Angle">0.0</span>°</div> | |
| </div> | |
| </div> | |
| <div id="eigenvector2Info" style="background-color: #eff6ff; border: 2px solid #bfdbfe; padding: 12px; border-radius: 6px; margin-bottom: 12px;"> | |
| <div style="font-family: 'Courier New', monospace; font-size: 0.875rem;"> | |
| <div><strong>PC2: [<span id="pc2Vector">0.000000, 0.000000</span>]</strong></div> | |
| <div style="color: #2563eb;">Angle: <span id="pc2Angle">0.0</span>°</div> | |
| </div> | |
| </div> | |
| <div style="font-size: 0.875rem; background-color: #fefce8; padding: 12px; border-radius: 6px; border: 1px solid #fde047;"> | |
| <h5 style="font-weight: 600; margin-bottom: 4px;">Key Properties:</h5> | |
| <ul style="list-style-type: disc; padding-left: 20px; margin-top: 4px;"> | |
| <li>PC1 has the highest eigenvalue (most variance)</li> | |
| <li>PCs are orthogonal (perpendicular) to each other</li> | |
| <li>Eigenvectors are unit vectors (normalized)</li> | |
| <li>Red line shows direction of maximum variance</li> | |
| <li>Blue line shows direction of minimum variance</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 5: Transformed Data --> | |
| <div class="step-section"> | |
| <h3 class="step-title">Step 5: Data in PC Space</h3> | |
| <div class="grid-2"> | |
| <div class="chart-container"> | |
| <canvas id="transformedChart"></canvas> | |
| </div> | |
| <div> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">Transformed Coordinates:</h4> | |
| <div class="data-display" id="transformedData"></div> | |
| <div class="stats-display"> | |
| <h5 style="font-weight: 600; margin-bottom: 4px;">Transformation Process:</h5> | |
| <p style="font-size: 0.875rem;">New coordinates = Original_centered × Eigenvector_matrix</p> | |
| <p style="font-size: 0.875rem; margin-top: 4px;">X-axis = PC1 (most variance)</p> | |
| <p style="font-size: 0.875rem;">Y-axis = PC2 (remaining variance)</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Mathematical Explanation --> | |
| <div style="margin-top: 24px; padding: 20px; background-color: #fefce8; border-radius: 12px; border: 2px solid #fde047;"> | |
| <h3 class="yellow-title" style="font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;">Complete Mathematical Process</h3> | |
| <div class="grid-2" style="font-size: 0.875rem;"> | |
| <div> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">1. Data Matrix (X):</h4> | |
| <p style="font-size: 0.875rem; margin-bottom: 8px;">Each row is a data point (augmented vector):</p> | |
| <div class="data-display" id="mathOriginalData"></div> | |
| <h4 style="font-weight: 600; margin-bottom: 8px; margin-top: 12px;">2. Centering:</h4> | |
| <div class="stats-display"> | |
| <p><strong>X_centered = X - mean(X)</strong></p> | |
| <p>Mean: <span id="mathMeanDisplay">[0.000, 0.000]</span></p> | |
| </div> | |
| <h4 style="font-weight: 600; margin-bottom: 8px; margin-top: 12px;">3. Covariance Matrix:</h4> | |
| <div class="data-display" id="mathCovMatrix"></div> | |
| </div> | |
| <div> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">4. Eigendecomposition:</h4> | |
| <div style="background-color: white; padding: 12px; border-radius: 6px; font-size: 0.875rem; margin-bottom: 8px; border: 1px solid #e5e7eb;"> | |
| <p><strong>Characteristic equation:</strong></p> | |
| <p style="font-family: 'Courier New', monospace;">det(C - λI) = 0</p> | |
| <p id="charEq" style="font-family: 'Courier New', monospace; color: #7c3aed;">λ² - 0.0000λ + 0.0000 = 0</p> | |
| </div> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">5. Exact Solutions:</h4> | |
| <div id="mathPC1" style="background-color: #fef2f2; padding: 12px; border-radius: 6px; font-size: 0.875rem; margin-bottom: 8px; border: 1px solid #fecaca;"> | |
| <p><strong>PC1 (λ=0.000000):</strong></p> | |
| <p style="font-family: 'Courier New', monospace;">Eigenvector: [0.000000, 0.000000]</p> | |
| </div> | |
| <div id="mathPC2" style="background-color: #eff6ff; padding: 12px; border-radius: 6px; font-size: 0.875rem; margin-bottom: 8px; border: 1px solid #bfdbfe;"> | |
| <p><strong>PC2 (λ=0.000000):</strong></p> | |
| <p style="font-family: 'Courier New', monospace;">Eigenvector: [0.000000, 0.000000]</p> | |
| </div> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">6. Verification:</h4> | |
| <div style="background-color: #f0fdf4; padding: 12px; border-radius: 6px; font-size: 0.875rem; border: 1px solid #bbf7d0;"> | |
| <p>C × v1 = λ1 × v1 ✓</p> | |
| <p>C × v2 = λ2 × v2 ✓</p> | |
| <p>v1 · v2 = <span id="orthogonalCheck">0.000000</span> (≈0, orthogonal)</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Understanding Results --> | |
| <div style="margin-top: 24px; padding: 20px; background-color: #f0fdf4; border-radius: 12px; border: 2px solid #bbf7d0;"> | |
| <h3 class="green-title" style="font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;">Understanding the Results</h3> | |
| <div style="font-size: 0.875rem; display: grid; gap: 12px;"> | |
| <div style="padding: 16px; background-color: white; border-radius: 8px; border: 1px solid #e5e7eb; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"> | |
| <p><strong>Augmented Vectors:</strong> Our data points [x, y] are 2D vectors. When we arrange them in a matrix where each row is one observation, we create an "augmented" data matrix. This allows us to process multiple data points simultaneously.</p> | |
| </div> | |
| <div style="padding: 16px; background-color: white; border-radius: 8px; border: 1px solid #e5e7eb; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"> | |
| <p><strong>Eigenvectors (Principal Components):</strong> These are special directions in the data space. The red line (PC1) points in the direction where data varies the most. The blue line (PC2) is perpendicular and shows the direction of remaining variance.</p> | |
| </div> | |
| <div style="padding: 16px; background-color: white; border-radius: 8px; border: 1px solid #e5e7eb; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"> | |
| <p><strong>Eigenvalues:</strong> These numbers (λ1, λ2) tell us exactly how much variance exists along each eigenvector direction. Larger eigenvalue = more important direction. The ratio λ1/(λ1+λ2) shows the percentage of total variance captured by PC1.</p> | |
| </div> | |
| <div style="padding: 16px; background-color: white; border-radius: 8px; border: 1px solid #e5e7eb; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"> | |
| <p><strong>PCA Transformation:</strong> We project each centered data point onto the principal component axes using the exact eigenvector coordinates. This rotates our coordinate system to align with the data's natural variation patterns.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Dataset Information --> | |
| <div style="margin-top: 24px; padding: 20px; background-color: #faf5ff; border-radius: 12px; border: 2px solid #e9d5ff;"> | |
| <h3 class="purple-title" style="font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;">Dataset Characteristics</h3> | |
| <div class="info-grid" style="font-size: 0.875rem;"> | |
| <div class="info-card"> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">Data Range:</h4> | |
| <p id="dataRangeX">X: [0.00, 0.00]</p> | |
| <p id="dataRangeY">Y: [0.00, 0.00]</p> | |
| </div> | |
| <div class="info-card"> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">Correlation:</h4> | |
| <p>Cov(X,Y) = <span id="covXY">0.0000</span></p> | |
| <p>Correlation = <span id="correlation">0.0000</span></p> | |
| </div> | |
| <div class="info-card"> | |
| <h4 style="font-weight: 600; margin-bottom: 8px;">Noise Effect:</h4> | |
| <p>Current Noise: <span id="displayNoise">0%</span></p> | |
| <p style="font-size: 0.8rem; color: #6b7280;">Add noise to see how PCA handles real-world data</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Global variables | |
| let dataPoints = [ | |
| [2, 3], [3, 4], [4, 5], [5, 4], [6, 6], [4, 2], [5, 3], [3, 5] | |
| ]; | |
| let baseDataPoints = [...dataPoints]; // Store clean data for noise application | |
| let selectedDataset = 'default'; | |
| let currentNoiseLevel = 0; | |
| const predefinedDatasets = { | |
| 'default': [ | |
| [2, 3], [3, 4], [4, 5], [5, 4], [6, 6], [4, 2], [5, 3], [3, 5] | |
| ], | |
| 'horizontal': [ | |
| [1, 3], [2, 3.1], [3, 2.9], [4, 3.2], [5, 2.8], [6, 3.1], [7, 2.9], [8, 3.0] | |
| ], | |
| 'vertical': [ | |
| [3, 1], [3.1, 2], [2.9, 3], [3.2, 4], [2.8, 5], [3.1, 6], [2.9, 7], [3.0, 8] | |
| ], | |
| 'diagonal': [ | |
| [1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6], [7, 7], [8, 8] | |
| ], | |
| 'circle': [ | |
| [4, 6], [5.4, 5.4], [6, 4], [5.4, 2.6], [4, 2], [2.6, 2.6], [2, 4], [2.6, 5.4] | |
| ], | |
| 'L_shape': [ | |
| [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [3, 2], [4, 2], [5, 2], [6, 2] | |
| ], | |
| 'clustered': [ | |
| [2, 2], [2.2, 2.1], [1.8, 2.2], [6, 6], [6.1, 5.9], [5.9, 6.1], [2, 6], [6, 2] | |
| ] | |
| }; | |
| let charts = {}; | |
| // Mathematical functions | |
| function calculateMean(data) { | |
| const n = data.length; | |
| const meanX = data.reduce((sum, point) => sum + point[0], 0) / n; | |
| const meanY = data.reduce((sum, point) => sum + point[1], 0) / n; | |
| return [meanX, meanY]; | |
| } | |
| function calculateCovarianceMatrix(data) { | |
| const n = data.length; | |
| let cov_xx = 0, cov_yy = 0, cov_xy = 0; | |
| for (let i = 0; i < n; i++) { | |
| cov_xx += data[i][0] * data[i][0]; | |
| cov_yy += data[i][1] * data[i][1]; | |
| cov_xy += data[i][0] * data[i][1]; | |
| } | |
| return [ | |
| [cov_xx / (n - 1), cov_xy / (n - 1)], | |
| [cov_xy / (n - 1), cov_yy / (n - 1)] | |
| ]; | |
| } | |
| function calculateEigenvaluesAndVectors(matrix) { | |
| const a = matrix[0][0]; | |
| const b = matrix[0][1]; | |
| const c = matrix[1][0]; | |
| const d = matrix[1][1]; | |
| // Characteristic polynomial: λ² - (a+d)λ + (ad-bc) = 0 | |
| const trace = a + d; | |
| const det = a * d - b * c; | |
| // Eigenvalues using quadratic formula | |
| const discriminant = trace * trace - 4 * det; | |
| const lambda1 = (trace + Math.sqrt(discriminant)) / 2; | |
| const lambda2 = (trace - Math.sqrt(discriminant)) / 2; | |
| // Eigenvectors | |
| let v1, v2; | |
| if (Math.abs(b) > 1e-10) { | |
| v1 = [1, (lambda1 - a) / b]; | |
| v2 = [1, (lambda2 - a) / b]; | |
| } else if (Math.abs(c) > 1e-10) { | |
| v1 = [(lambda1 - d) / c, 1]; | |
| v2 = [(lambda2 - d) / c, 1]; | |
| } else { | |
| v1 = [1, 0]; | |
| v2 = [0, 1]; | |
| } | |
| // Normalize eigenvectors | |
| const norm1 = Math.sqrt(v1[0] * v1[0] + v1[1] * v1[1]); | |
| const norm2 = Math.sqrt(v2[0] * v2[0] + v2[1] * v2[1]); | |
| v1 = [v1[0] / norm1, v1[1] / norm1]; | |
| v2 = [v2[0] / norm2, v2[1] / norm2]; | |
| return { | |
| eigenvalues: [lambda1, lambda2], | |
| eigenvectors: [v1, v2] | |
| }; | |
| } | |
| function projectData(data, eigenvectors) { | |
| return data.map(point => [ | |
| point[0] * eigenvectors[0][0] + point[1] * eigenvectors[0][1], // PC1 | |
| point[0] * eigenvectors[1][0] + point[1] * eigenvectors[1][1] // PC2 | |
| ]); | |
| } | |
| // Chart creation functions | |
| function createScatterChart(canvasId, data, options = {}) { | |
| const canvas = document.getElementById(canvasId); | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| if (charts[canvasId]) { | |
| charts[canvasId].destroy(); | |
| } | |
| const chartData = data.map((point, i) => ({ x: point[0], y: point[1] })); | |
| charts[canvasId] = new Chart(ctx, { | |
| type: 'scatter', | |
| data: { | |
| datasets: [{ | |
| data: chartData, | |
| backgroundColor: options.color || '#8884d8', | |
| borderColor: options.borderColor || '#4a5568', | |
| borderWidth: 2, | |
| pointRadius: 8, | |
| pointHoverRadius: 12, | |
| pointBorderWidth: 2 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| animation: { | |
| duration: 300 | |
| }, | |
| plugins: { | |
| legend: { display: false }, | |
| tooltip: { | |
| backgroundColor: 'rgba(0,0,0,0.8)', | |
| titleColor: 'white', | |
| bodyColor: 'white', | |
| borderColor: options.color || '#8884d8', | |
| borderWidth: 1, | |
| callbacks: { | |
| label: function(context) { | |
| return `Point ${context.dataIndex + 1}: (${context.parsed.x.toFixed(2)}, ${context.parsed.y.toFixed(2)})`; | |
| } | |
| } | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| type: 'linear', | |
| min: options.xMin || 0, | |
| max: options.xMax || 8, | |
| grid: { | |
| color: '#e5e7eb', | |
| lineWidth: 1 | |
| }, | |
| border: { | |
| color: '#9ca3af', | |
| width: 2 | |
| }, | |
| title: { | |
| display: true, | |
| text: options.xLabel || 'X', | |
| font: { size: 14, weight: 'bold' } | |
| }, | |
| ticks: { | |
| font: { size: 12 } | |
| } | |
| }, | |
| y: { | |
| type: 'linear', | |
| min: options.yMin || 0, | |
| max: options.yMax || 8, | |
| grid: { | |
| color: '#e5e7eb', | |
| lineWidth: 1 | |
| }, | |
| border: { | |
| color: '#9ca3af', | |
| width: 2 | |
| }, | |
| title: { | |
| display: true, | |
| text: options.yLabel || 'Y', | |
| font: { size: 14, weight: 'bold' } | |
| }, | |
| ticks: { | |
| font: { size: 12 } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| function updateSVGVisualization(centeredData, eigenvectors) { | |
| const svg = document.getElementById('eigenvectorSVG'); | |
| // Clear existing content except grid and axes | |
| const existingElements = svg.querySelectorAll('.data-point, .pc-line, .pc-arrow, .pc-label'); | |
| existingElements.forEach(el => el.remove()); | |
| // Add data points | |
| centeredData.forEach((point, i) => { | |
| const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); | |
| circle.setAttribute('cx', point[0]); | |
| circle.setAttribute('cy', -point[1]); // SVG y-axis is flipped | |
| circle.setAttribute('r', '0.12'); | |
| circle.setAttribute('fill', '#10b981'); | |
| circle.setAttribute('stroke', '#047857'); | |
| circle.setAttribute('stroke-width', '0.03'); | |
| circle.setAttribute('class', 'data-point'); | |
| svg.appendChild(circle); | |
| }); | |
| // PC1 Line (Red) - thicker and more visible | |
| const pc1Line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); | |
| pc1Line.setAttribute('x1', -3.5 * eigenvectors[0][0]); | |
| pc1Line.setAttribute('y1', 3.5 * eigenvectors[0][1]); | |
| pc1Line.setAttribute('x2', 3.5 * eigenvectors[0][0]); | |
| pc1Line.setAttribute('y2', -3.5 * eigenvectors[0][1]); | |
| pc1Line.setAttribute('stroke', '#ef4444'); | |
| pc1Line.setAttribute('stroke-width', '0.12'); | |
| pc1Line.setAttribute('class', 'pc-line'); | |
| svg.appendChild(pc1Line); | |
| // PC2 Line (Blue) - thicker and more visible | |
| const pc2Line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); | |
| pc2Line.setAttribute('x1', -3.5 * eigenvectors[1][0]); | |
| pc2Line.setAttribute('y1', 3.5 * eigenvectors[1][1]); | |
| pc2Line.setAttribute('x2', 3.5 * eigenvectors[1][0]); | |
| pc2Line.setAttribute('y2', -3.5 * eigenvectors[1][1]); | |
| pc2Line.setAttribute('stroke', '#3b82f6'); | |
| pc2Line.setAttribute('stroke-width', '0.12'); | |
| pc2Line.setAttribute('class', 'pc-line'); | |
| svg.appendChild(pc2Line); | |
| // Arrow heads for PC1 - larger | |
| const pc1Arrow = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); | |
| const pc1X = 3.5 * eigenvectors[0][0]; | |
| const pc1Y = -3.5 * eigenvectors[0][1]; | |
| pc1Arrow.setAttribute('points', `${pc1X},${pc1Y} ${pc1X - 0.3},${pc1Y + 0.15} ${pc1X - 0.3},${pc1Y - 0.15}`); | |
| pc1Arrow.setAttribute('fill', '#ef4444'); | |
| pc1Arrow.setAttribute('class', 'pc-arrow'); | |
| svg.appendChild(pc1Arrow); | |
| // Arrow heads for PC2 - larger | |
| const pc2Arrow = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); | |
| const pc2X = 3.5 * eigenvectors[1][0]; | |
| const pc2Y = -3.5 * eigenvectors[1][1]; | |
| pc2Arrow.setAttribute('points', `${pc2X},${pc2Y} ${pc2X - 0.3},${pc2Y + 0.15} ${pc2X - 0.3},${pc2Y - 0.15}`); | |
| pc2Arrow.setAttribute('fill', '#3b82f6'); | |
| pc2Arrow.setAttribute('class', 'pc-arrow'); | |
| svg.appendChild(pc2Arrow); | |
| // Labels - larger and more visible | |
| const pc1Label = document.createElementNS('http://www.w3.org/2000/svg', 'text'); | |
| pc1Label.setAttribute('x', 2.8 * eigenvectors[0][0]); | |
| pc1Label.setAttribute('y', -2.8 * eigenvectors[0][1]); | |
| pc1Label.setAttribute('fill', '#ef4444'); | |
| pc1Label.setAttribute('font-size', '0.4'); | |
| pc1Label.setAttribute('font-weight', 'bold'); | |
| pc1Label.setAttribute('class', 'pc-label'); | |
| pc1Label.textContent = 'PC1'; | |
| svg.appendChild(pc1Label); | |
| const pc2Label = document.createElementNS('http://www.w3.org/2000/svg', 'text'); | |
| pc2Label.setAttribute('x', 2.8 * eigenvectors[1][0]); | |
| pc2Label.setAttribute('y', -2.8 * eigenvectors[1][1]); | |
| pc2Label.setAttribute('fill', '#3b82f6'); | |
| pc2Label.setAttribute('font-size', '0.4'); | |
| pc2Label.setAttribute('font-weight', 'bold'); | |
| pc2Label.setAttribute('class', 'pc-label'); | |
| pc2Label.textContent = 'PC2'; | |
| svg.appendChild(pc2Label); | |
| } | |
| // Noise functions | |
| function updateNoise() { | |
| const slider = document.getElementById('noiseSlider'); | |
| const value = parseInt(slider.value); | |
| currentNoiseLevel = value; | |
| document.getElementById('noiseValue').textContent = `${value}%`; | |
| document.getElementById('currentNoise').textContent = `${value}%`; | |
| document.getElementById('displayNoise').textContent = `${value}%`; | |
| // Update color based on noise level | |
| const noiseDisplay = document.getElementById('noiseValue'); | |
| if (value === 0) { | |
| noiseDisplay.style.color = '#10b981'; // Green for no noise | |
| } else if (value <= 30) { | |
| noiseDisplay.style.color = '#f59e0b'; // Yellow for low noise | |
| } else { | |
| noiseDisplay.style.color = '#ef4444'; // Red for high noise | |
| } | |
| } | |
| function applyNoise() { | |
| const noiseLevel = currentNoiseLevel / 100; | |
| // Apply noise to base data | |
| dataPoints = baseDataPoints.map(point => [ | |
| point[0] + (Math.random() - 0.5) * noiseLevel * 2, | |
| point[1] + (Math.random() - 0.5) * noiseLevel * 2 | |
| ]); | |
| updateVisualization(); | |
| } | |
| // Main update function | |
| function updateVisualization() { | |
| const mean = calculateMean(dataPoints); | |
| const centeredData = dataPoints.map(point => [point[0] - mean[0], point[1] - mean[1]]); | |
| const covMatrix = calculateCovarianceMatrix(centeredData); | |
| const eigen = calculateEigenvaluesAndVectors(covMatrix); | |
| const transformedData = projectData(centeredData, eigen.eigenvectors); | |
| // Update displays | |
| document.getElementById('pointCount').textContent = `${dataPoints.length} points`; | |
| document.getElementById('selectedDataset').textContent = selectedDataset.toUpperCase(); | |
| document.getElementById('meanDisplay').textContent = `[${mean[0].toFixed(2)}, ${mean[1].toFixed(2)}]`; | |
| // Update original data display | |
| document.getElementById('originalData').innerHTML = dataPoints.map((point, i) => | |
| `<div>Point ${i+1}: [${point[0].toFixed(2)}, ${point[1].toFixed(2)}]</div>` | |
| ).join(''); | |
| // Update centered data display | |
| document.getElementById('centeredData').innerHTML = centeredData.map((point, i) => | |
| `<div>Point ${i+1}: [${point[0].toFixed(3)}, ${point[1].toFixed(3)}]</div>` | |
| ).join(''); | |
| // Update covariance matrix display | |
| document.getElementById('covarianceMatrix').innerHTML = | |
| `<div style="margin-bottom: 4px;">[${covMatrix[0][0].toFixed(4)}, ${covMatrix[0][1].toFixed(4)}]</div>` + | |
| `<div>[${covMatrix[1][0].toFixed(4)}, ${covMatrix[1][1].toFixed(4)}]</div>`; | |
| document.getElementById('varianceExplanation').textContent = | |
| `Diagonal: Variance in X (${covMatrix[0][0].toFixed(3)}) and Y (${covMatrix[1][1].toFixed(3)})`; | |
| document.getElementById('covarianceExplanation').textContent = | |
| `Off-diagonal: Covariance between X and Y (${covMatrix[0][1].toFixed(3)})`; | |
| // Update eigenvalue and eigenvector displays | |
| const totalVariance = eigen.eigenvalues[0] + eigen.eigenvalues[1]; | |
| document.getElementById('lambda1').textContent = eigen.eigenvalues[0].toFixed(6); | |
| document.getElementById('lambda2').textContent = eigen.eigenvalues[1].toFixed(6); | |
| document.getElementById('variance1').textContent = ((eigen.eigenvalues[0] / totalVariance) * 100).toFixed(1); | |
| document.getElementById('variance2').textContent = ((eigen.eigenvalues[1] / totalVariance) * 100).toFixed(1); | |
| document.getElementById('pc1Vector').textContent = `${eigen.eigenvectors[0][0].toFixed(4)}, ${eigen.eigenvectors[0][1].toFixed(4)}`; | |
| document.getElementById('pc2Vector').textContent = `${eigen.eigenvectors[1][0].toFixed(4)}, ${eigen.eigenvectors[1][1].toFixed(4)}`; | |
| document.getElementById('pc1Angle').textContent = (Math.atan2(eigen.eigenvectors[0][1], eigen.eigenvectors[0][0]) * 180 / Math.PI).toFixed(1); | |
| document.getElementById('pc2Angle').textContent = (Math.atan2(eigen.eigenvectors[1][1], eigen.eigenvectors[1][0]) * 180 / Math.PI).toFixed(1); | |
| // Update transformed data display | |
| document.getElementById('transformedData').innerHTML = transformedData.map((point, i) => | |
| `<div>Point ${i+1}: [${point[0].toFixed(3)}, ${point[1].toFixed(3)}]</div>` | |
| ).join(''); | |
| // Update mathematical section | |
| document.getElementById('mathOriginalData').innerHTML = dataPoints.map((point, i) => | |
| `<div>Point ${i+1}: [${point[0].toFixed(2)}, ${point[1].toFixed(2)}]</div>` | |
| ).join(''); | |
| document.getElementById('mathMeanDisplay').textContent = `[${mean[0].toFixed(3)}, ${mean[1].toFixed(3)}]`; | |
| document.getElementById('mathCovMatrix').innerHTML = | |
| `<div>[${covMatrix[0][0].toFixed(4)}, ${covMatrix[0][1].toFixed(4)}]</div>` + | |
| `<div>[${covMatrix[1][0].toFixed(4)}, ${covMatrix[1][1].toFixed(4)}]</div>`; | |
| const trace = covMatrix[0][0] + covMatrix[1][1]; | |
| const det = covMatrix[0][0] * covMatrix[1][1] - covMatrix[0][1] * covMatrix[1][0]; | |
| document.getElementById('charEq').textContent = `λ² - ${trace.toFixed(4)}λ + ${det.toFixed(4)} = 0`; | |
| document.getElementById('mathPC1').innerHTML = | |
| `<p><strong>PC1 (λ=${eigen.eigenvalues[0].toFixed(6)}):</strong></p>` + | |
| `<p style="font-family: 'Courier New', monospace;">Eigenvector: [${eigen.eigenvectors[0][0].toFixed(6)}, ${eigen.eigenvectors[0][1].toFixed(6)}]</p>`; | |
| document.getElementById('mathPC2').innerHTML = | |
| `<p><strong>PC2 (λ=${eigen.eigenvalues[1].toFixed(6)}):</strong></p>` + | |
| `<p style="font-family: 'Courier New', monospace;">Eigenvector: [${eigen.eigenvectors[1][0].toFixed(6)}, ${eigen.eigenvectors[1][1].toFixed(6)}]</p>`; | |
| const dotProduct = eigen.eigenvectors[0][0] * eigen.eigenvectors[1][0] + eigen.eigenvectors[0][1] * eigen.eigenvectors[1][1]; | |
| document.getElementById('orthogonalCheck').textContent = dotProduct.toFixed(6); | |
| // Update dataset characteristics | |
| document.getElementById('currentDatasetName').textContent = selectedDataset.toUpperCase(); | |
| document.getElementById('currentPointCount').textContent = `${dataPoints.length} points`; | |
| document.getElementById('dataPointCount').textContent = dataPoints.length; | |
| const xValues = dataPoints.map(p => p[0]); | |
| const yValues = dataPoints.map(p => p[1]); | |
| document.getElementById('dataRangeX').textContent = `X: [${Math.min(...xValues).toFixed(2)}, ${Math.max(...xValues).toFixed(2)}]`; | |
| document.getElementById('dataRangeY').textContent = `Y: [${Math.min(...yValues).toFixed(2)}, ${Math.max(...yValues).toFixed(2)}]`; | |
| document.getElementById('covXY').textContent = covMatrix[0][1].toFixed(4); | |
| const correlation = covMatrix[0][1] / Math.sqrt(covMatrix[0][0] * covMatrix[1][1]); | |
| document.getElementById('correlation').textContent = correlation.toFixed(4); | |
| // Update all charts with proper timing and distinct styling | |
| setTimeout(() => { | |
| createScatterChart('originalChart', dataPoints, { | |
| color: '#8b5cf6', | |
| borderColor: '#7c3aed', | |
| xMin: 0, | |
| xMax: 8, | |
| yMin: 0, | |
| yMax: 8, | |
| xLabel: 'X (Original)', | |
| yLabel: 'Y (Original)' | |
| }); | |
| }, 50); | |
| setTimeout(() => { | |
| createScatterChart('centeredChart', centeredData, { | |
| color: '#10b981', | |
| borderColor: '#047857', | |
| xMin: -4, | |
| xMax: 4, | |
| yMin: -4, | |
| yMax: 4, | |
| xLabel: 'X (Centered)', | |
| yLabel: 'Y (Centered)' | |
| }); | |
| }, 100); | |
| setTimeout(() => { | |
| createScatterChart('transformedChart', transformedData, { | |
| color: '#f59e0b', | |
| borderColor: '#d97706', | |
| xMin: -5, | |
| xMax: 5, | |
| yMin: -3, | |
| yMax: 3, | |
| xLabel: 'PC1 (First Principal Component)', | |
| yLabel: 'PC2 (Second Principal Component)' | |
| }); | |
| }, 150); | |
| setTimeout(() => { | |
| updateSVGVisualization(centeredData, eigen.eigenvectors); | |
| }, 200); | |
| } | |
| // Control functions | |
| function loadDataset(datasetName) { | |
| selectedDataset = datasetName; | |
| baseDataPoints = [...predefinedDatasets[datasetName]]; | |
| dataPoints = [...baseDataPoints]; | |
| // Reset noise | |
| currentNoiseLevel = 0; | |
| document.getElementById('noiseSlider').value = 0; | |
| updateNoise(); | |
| // Update button states | |
| document.querySelectorAll('.btn-dataset').forEach(btn => { | |
| btn.classList.remove('active'); | |
| }); | |
| document.querySelector(`[data-dataset="${datasetName}"]`).classList.add('active'); | |
| updateVisualization(); | |
| } | |
| function createNewRandomDataset() { | |
| const newPoints = []; | |
| const numPoints = Math.floor(Math.random() * 8) + 8; // 8-15 points | |
| const centerX = Math.random() * 4 + 2; | |
| const centerY = Math.random() * 4 + 2; | |
| const spread = Math.random() * 2 + 1; | |
| for (let i = 0; i < numPoints; i++) { | |
| newPoints.push([ | |
| centerX + (Math.random() - 0.5) * spread * 2, | |
| centerY + (Math.random() - 0.5) * spread * 2 | |
| ]); | |
| } | |
| baseDataPoints = [...newPoints]; | |
| dataPoints = [...newPoints]; | |
| selectedDataset = 'custom'; | |
| // Reset noise | |
| currentNoiseLevel = 0; | |
| document.getElementById('noiseSlider').value = 0; | |
| updateNoise(); | |
| // Update button states | |
| document.querySelectorAll('.btn-dataset').forEach(btn => { | |
| btn.classList.remove('active'); | |
| }); | |
| updateVisualization(); | |
| } | |
| function addRandomPoint() { | |
| const newPoint = [ | |
| Math.random() * 6 + 1, | |
| Math.random() * 6 + 1 | |
| ]; | |
| baseDataPoints.push(newPoint); | |
| dataPoints.push([...newPoint]); | |
| // Reapply current noise level | |
| if (currentNoiseLevel > 0) { | |
| applyNoise(); | |
| } else { | |
| updateVisualization(); | |
| } | |
| } | |
| function removeLastPoint() { | |
| if (dataPoints.length > 2) { | |
| baseDataPoints.pop(); | |
| dataPoints.pop(); | |
| // Reapply current noise level | |
| if (currentNoiseLevel > 0) { | |
| applyNoise(); | |
| } else { | |
| updateVisualization(); | |
| } | |
| } | |
| } | |
| // Initialize page | |
| function initializePage() { | |
| // Create dataset buttons | |
| const buttonContainer = document.getElementById('datasetButtons'); | |
| Object.keys(predefinedDatasets).forEach(datasetName => { | |
| const button = document.createElement('button'); | |
| button.className = `btn btn-dataset ${datasetName === 'default' ? 'active' : ''}`; | |
| button.setAttribute('data-dataset', datasetName); | |
| button.textContent = datasetName.replace('_', ' ').toUpperCase(); | |
| button.onclick = () => loadDataset(datasetName); | |
| buttonContainer.appendChild(button); | |
| }); | |
| // Initialize noise slider | |
| updateNoise(); | |
| // Initial visualization update | |
| setTimeout(() => { | |
| updateVisualization(); | |
| }, 100); | |
| } | |
| // Run when page loads | |
| document.addEventListener('DOMContentLoaded', initializePage); | |
| </script> | |
| </body> | |
| </html>4 style="font-weight: 600; margin-bottom: 8px;">Current Dataset:</h4> | |
| <p><strong id="currentDatasetName">DEFAULT</strong></p> | |
| <p id="currentPointCount">8 points</p> | |
| <p style="color: #7c3aed;">Try different shapes to see how PCA adapts!</p> | |
| </div> | |
| <div class="info-card"> | |
| <h |