Commit ·
09edf62
1
Parent(s): 4e4036b
Add Fourier & linear pages; unify styles
Browse filesAdd new Fourier Transform and Linear Regression HTML pages with their page-specific CSS and JS, and register shared utilities in a new src/js/common.js. Refactor global styling (src/css/style.css) to a unified design system, remove legacy src/css/inverse_style.css, and add page CSS for fourier and linear-regression. Update direct_classifier.html and inverse_classifier.html titles/labels and include common.js, and update README to document new pages, structure, usage and bump project version to 0.2.0.
- README.md +72 -44
- direct_classifier.html +8 -7
- fourier_transform.html +219 -0
- inverse_classifier.html +9 -8
- linear_regression.html +113 -0
- src/css/fourier_transform.css +101 -0
- src/css/inverse_style.css +0 -237
- src/css/linear_regression.css +99 -0
- src/css/style.css +394 -58
- src/js/common.js +205 -0
- src/js/direct_classifier.js +12 -146
- src/js/fourier_transform.js +595 -0
- src/js/inverse_classifier.js +2 -140
- src/js/linear_regression.js +730 -0
README.md
CHANGED
|
@@ -1,74 +1,102 @@
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
title: School of Statistics - Interactive Classification Dashboards
|
| 4 |
-
emoji: 📊
|
| 5 |
colorFrom: blue
|
| 6 |
colorTo: indigo
|
| 7 |
sdk: static
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
-
# School of Statistics
|
| 12 |
-
|
| 13 |
-

|
| 14 |
|
| 15 |
-
|
| 16 |
|
| 17 |
-
##
|
| 18 |
|
| 19 |
-
###
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
* Key performance metrics (Accuracy, Precision, Recall, etc.).
|
| 30 |
-
* A detailed confusion matrix.
|
| 31 |
|
| 32 |
-
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
|
| 38 |
```
|
| 39 |
.
|
| 40 |
-
├── direct_classifier.html
|
| 41 |
-
├── inverse_classifier.html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
├── LICENSE
|
| 43 |
├── README.md
|
| 44 |
-
└── src
|
| 45 |
-
├── assets
|
| 46 |
│ └── logo.jpg
|
| 47 |
-
├── css
|
| 48 |
-
│ ├──
|
| 49 |
-
│
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
├── direct_classifier.js
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
```
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
|
| 65 |
|
| 66 |
-
|
| 67 |
-
2. Open either `direct_classifier.html` or `inverse_classifier.html` in your web browser.
|
| 68 |
-
3. No local server is needed! All the logic is self-contained in the HTML, CSS, and JavaScript files.
|
| 69 |
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
-
##
|
| 73 |
|
| 74 |
-
|
|
|
|
| 1 |
---
|
| 2 |
+
title: School of Statistics
|
|
|
|
|
|
|
| 3 |
colorFrom: blue
|
| 4 |
colorTo: indigo
|
| 5 |
sdk: static
|
| 6 |
pinned: false
|
| 7 |
---
|
| 8 |
|
| 9 |
+
# School of Statistics
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
Interactive visualizations for exploring statistical and machine learning concepts. Each page runs entirely in the browser (HTML, CSS, JavaScript with Chart.js) without requiring a server or build step.
|
| 12 |
|
| 13 |
+
## Available Pages
|
| 14 |
|
| 15 |
+
### Classification
|
| 16 |
|
| 17 |
+
| Page | Description |
|
| 18 |
+
|------|-------------|
|
| 19 |
+
| [Direct Classification](https://berangerthomas.github.io/SchoolOfStatistics/direct_classifier.html) | Generate synthetic 2D datasets and observe how class separation affects Gaussian Naive Bayes classifier performance. Displays ROC curve, AUC, confusion matrix, and standard metrics (accuracy, precision, recall, specificity, F1-score). |
|
| 20 |
+
| [Inverse Classification](https://berangerthomas.github.io/SchoolOfStatistics/inverse_classifier.html) | Directly set confusion matrix values (TP, FP, TN, FN) and observe resulting metrics, ROC curve, and simulated score distributions. Parameters can be locked to constrain totals. |
|
| 21 |
|
| 22 |
+
### Regression
|
| 23 |
|
| 24 |
+
| Page | Description |
|
| 25 |
+
|------|-------------|
|
| 26 |
+
| [Linear Regression](https://berangerthomas.github.io/SchoolOfStatistics/linear_regression.html) | Interactive point placement on canvas with linear or polynomial regression fitting. Displays residuals, coefficient of determination (R²), and regression diagnostics. Supports zoom, point dragging, and confidence band display. |
|
|
|
|
|
|
|
| 27 |
|
| 28 |
+
### Signal Processing
|
| 29 |
|
| 30 |
+
| Page | Description |
|
| 31 |
+
|------|-------------|
|
| 32 |
+
| [Fourier Transform](https://berangerthomas.github.io/SchoolOfStatistics/fourier_transform.html) | Compose signals from sine waves and visualize their frequency spectrum. Up to 4 components with frequency, amplitude, and phase control. Displays time-domain signal, magnitude spectrum, phase spectrum, and signal metrics (sampling rate, Nyquist frequency, frequency resolution, total power, RMS). |
|
| 33 |
|
| 34 |
+
## Project Structure
|
| 35 |
|
| 36 |
```
|
| 37 |
.
|
| 38 |
+
├── direct_classifier.html # Direct classification (Naive Bayes)
|
| 39 |
+
├── inverse_classifier.html # Inverse classification (confusion matrix)
|
| 40 |
+
├── logistic_regression.html # Logistic regression
|
| 41 |
+
├── linear_regression.html # Linear/polynomial regression
|
| 42 |
+
├── fourier_transform.html # Fourier transform
|
| 43 |
+
├── embedding_distances.html # Embedding space distances
|
| 44 |
+
├── IDEAS.md # Specifications for planned pages
|
| 45 |
+
├── CHANGELOG.md # Version history
|
| 46 |
├── LICENSE
|
| 47 |
├── README.md
|
| 48 |
+
└── src/
|
| 49 |
+
├── assets/
|
| 50 |
│ └── logo.jpg
|
| 51 |
+
├── css/
|
| 52 |
+
│ ├── style.css # Shared base styles
|
| 53 |
+
│ ├── direct_classifier.css # Page-specific styles
|
| 54 |
+
│ ├── embedding_distances.css
|
| 55 |
+
│ ├── fourier_transform.css
|
| 56 |
+
│ ├── linear_regression.css
|
| 57 |
+
│ └── logistic_regression.css
|
| 58 |
+
└── js/
|
| 59 |
+
├── common.js # Shared utilities (metrics, ROC, matrices, drag, etc.)
|
| 60 |
├── direct_classifier.js
|
| 61 |
+
├── embedding_distances.js
|
| 62 |
+
├── fourier_transform.js
|
| 63 |
+
├── inverse_classifier.js
|
| 64 |
+
├── linear_regression.js
|
| 65 |
+
└── logistic_regression.js
|
| 66 |
```
|
| 67 |
|
| 68 |
+
## Usage
|
| 69 |
+
|
| 70 |
+
1. Clone the repository.
|
| 71 |
+
2. Open any `.html` file in a web browser.
|
| 72 |
+
|
| 73 |
+
No dependencies to install — all libraries are loaded via CDN.
|
| 74 |
+
|
| 75 |
+
## Versioning
|
| 76 |
+
|
| 77 |
+
This project follows [Semantic Versioning](https://semver.org/). See [CHANGELOG.md](CHANGELOG.md) for release history.
|
| 78 |
+
|
| 79 |
+
Current version: **0.2.0**
|
| 80 |
+
|
| 81 |
+
## Roadmap
|
| 82 |
|
| 83 |
+
Detailed specifications for planned pages are documented in `IDEAS.md`.
|
| 84 |
|
| 85 |
+
### À venir
|
|
|
|
|
|
|
| 86 |
|
| 87 |
+
- **Bias-Variance Tradeoff Explorer**: visualize bias-variance decomposition with polynomial fitting of increasing degree
|
| 88 |
+
- **k-Nearest Neighbors Playground**: interactive point placement and k-NN decision boundary visualization
|
| 89 |
+
- **Gradient Descent Visualizer**: real-time navigation on 2D loss surfaces, optimizer comparison
|
| 90 |
+
- **Principal Component Analysis (PCA) Step-by-Step**: Gaussian cloud generation and principal component visualization
|
| 91 |
+
- **Clustering Algorithms Visualizer**: k-Means and DBSCAN comparison on various dataset shapes
|
| 92 |
+
- **Neural Network Architecture & Forward Pass Visualizer**: layer-by-layer fully-connected network construction
|
| 93 |
+
- **Tokenization & Embedding Visualizer**: tokenization and 2D embedding space projection
|
| 94 |
+
- **Attention Mechanism Visualizer**: Transformer attention mechanism visualization
|
| 95 |
+
- **Probability Distributions Explorer**: exploration of standard distributions (Normal, Uniform, Exponential, Poisson, Binomial, Beta, Gamma, Chi-squared)
|
| 96 |
+
- **Markov Chain Text Generator**: Markov chain construction and text generation
|
| 97 |
+
- **A/B Testing Calculator**: statistical tool for hypothesis testing
|
| 98 |
+
- **Voice Signal Waveform Analyzer**: audio recording, waveform display, spectrogram computation, and dominant frequency identification
|
| 99 |
|
| 100 |
+
## License
|
| 101 |
|
| 102 |
+
See the [LICENSE](LICENSE) file.
|
direct_classifier.html
CHANGED
|
@@ -4,17 +4,17 @@
|
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
-
<title>
|
| 8 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
| 9 |
<link rel="stylesheet" href="src/css/style.css">
|
| 10 |
</head>
|
| 11 |
|
| 12 |
<body>
|
| 13 |
<div class="container">
|
| 14 |
-
<h1>
|
| 15 |
|
| 16 |
<div class="floating-controls" id="floatingControls">
|
| 17 |
-
<div class="controls-title" id="controlsTitle"
|
| 18 |
<div class="controls-grid">
|
| 19 |
<div class="control-group">
|
| 20 |
<label for="separationSlider" class="control-label">Class Separation</label>
|
|
@@ -31,15 +31,15 @@
|
|
| 31 |
|
| 32 |
<div class="charts-container">
|
| 33 |
<div class="chart-card">
|
| 34 |
-
<div class="chart-title"
|
| 35 |
<div class="chart-container"><canvas id="rocChart"></canvas></div>
|
| 36 |
</div>
|
| 37 |
<div class="chart-card">
|
| 38 |
-
<div class="chart-title"
|
| 39 |
<div class="chart-container"><canvas id="metricsChart"></canvas></div>
|
| 40 |
</div>
|
| 41 |
<div class="chart-card">
|
| 42 |
-
<div class="chart-title"
|
| 43 |
<div class="confusion-matrix-layout">
|
| 44 |
<div class="matrix-ylabel">TRUE class</div>
|
| 45 |
<div class="matrix-canvas-container">
|
|
@@ -49,7 +49,7 @@
|
|
| 49 |
</div>
|
| 50 |
</div>
|
| 51 |
<div class="chart-card">
|
| 52 |
-
<div class="chart-title"
|
| 53 |
<div class="chart-container"><canvas id="dataChart"></canvas></div>
|
| 54 |
</div>
|
| 55 |
</div>
|
|
@@ -61,6 +61,7 @@
|
|
| 61 |
rel="noopener noreferrer">Project's GitHub</a>
|
| 62 |
</footer>
|
| 63 |
|
|
|
|
| 64 |
<script src="src/js/direct_classifier.js"></script>
|
| 65 |
</body>
|
| 66 |
|
|
|
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Direct Classification Dashboard</title>
|
| 8 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
| 9 |
<link rel="stylesheet" href="src/css/style.css">
|
| 10 |
</head>
|
| 11 |
|
| 12 |
<body>
|
| 13 |
<div class="container">
|
| 14 |
+
<h1>Direct Classification Dashboard</h1>
|
| 15 |
|
| 16 |
<div class="floating-controls" id="floatingControls">
|
| 17 |
+
<div class="controls-title" id="controlsTitle">Data Controls</div>
|
| 18 |
<div class="controls-grid">
|
| 19 |
<div class="control-group">
|
| 20 |
<label for="separationSlider" class="control-label">Class Separation</label>
|
|
|
|
| 31 |
|
| 32 |
<div class="charts-container">
|
| 33 |
<div class="chart-card">
|
| 34 |
+
<div class="chart-title">ROC Curve & AUC</div>
|
| 35 |
<div class="chart-container"><canvas id="rocChart"></canvas></div>
|
| 36 |
</div>
|
| 37 |
<div class="chart-card">
|
| 38 |
+
<div class="chart-title">Performance Metrics</div>
|
| 39 |
<div class="chart-container"><canvas id="metricsChart"></canvas></div>
|
| 40 |
</div>
|
| 41 |
<div class="chart-card">
|
| 42 |
+
<div class="chart-title">Confusion Matrix</div>
|
| 43 |
<div class="confusion-matrix-layout">
|
| 44 |
<div class="matrix-ylabel">TRUE class</div>
|
| 45 |
<div class="matrix-canvas-container">
|
|
|
|
| 49 |
</div>
|
| 50 |
</div>
|
| 51 |
<div class="chart-card">
|
| 52 |
+
<div class="chart-title">Data Distribution</div>
|
| 53 |
<div class="chart-container"><canvas id="dataChart"></canvas></div>
|
| 54 |
</div>
|
| 55 |
</div>
|
|
|
|
| 61 |
rel="noopener noreferrer">Project's GitHub</a>
|
| 62 |
</footer>
|
| 63 |
|
| 64 |
+
<script src="src/js/common.js"></script>
|
| 65 |
<script src="src/js/direct_classifier.js"></script>
|
| 66 |
</body>
|
| 67 |
|
fourier_transform.html
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Fourier Transform Visualizer</title>
|
| 8 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
| 9 |
+
<link rel="stylesheet" href="src/css/style.css">
|
| 10 |
+
<link rel="stylesheet" href="src/css/fourier_transform.css">
|
| 11 |
+
</head>
|
| 12 |
+
|
| 13 |
+
<body>
|
| 14 |
+
<div class="container">
|
| 15 |
+
<h1>Fourier Transform Visualizer</h1>
|
| 16 |
+
<p class="subtitle">Compose signals from sine waves and visualize their frequency spectrum</p>
|
| 17 |
+
|
| 18 |
+
<div class="floating-controls" id="floatingControls">
|
| 19 |
+
<div class="controls-title" id="controlsTitle">Signal Controls</div>
|
| 20 |
+
<div class="controls-grid">
|
| 21 |
+
<!-- Wave 1 -->
|
| 22 |
+
<div class="wave-section">
|
| 23 |
+
<div class="wave-header">
|
| 24 |
+
<label class="control-label">
|
| 25 |
+
<input type="checkbox" id="enableWave1" class="checkbox-input" checked>
|
| 26 |
+
Wave 1
|
| 27 |
+
</label>
|
| 28 |
+
<span class="wave-color" style="background-color: #1976d2;"></span>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="control-group">
|
| 31 |
+
<label for="freq1" class="control-label">Frequency (Hz)</label>
|
| 32 |
+
<input type="range" min="0" max="50" value="5" step="1" class="slider" id="freq1">
|
| 33 |
+
<div class="slider-value" id="freq1Value">5 Hz</div>
|
| 34 |
+
</div>
|
| 35 |
+
<div class="control-group">
|
| 36 |
+
<label for="amp1" class="control-label">Amplitude</label>
|
| 37 |
+
<input type="range" min="0" max="10" value="5" step="0.1" class="slider" id="amp1">
|
| 38 |
+
<div class="slider-value" id="amp1Value">5.0</div>
|
| 39 |
+
</div>
|
| 40 |
+
<div class="control-group">
|
| 41 |
+
<label for="phase1" class="control-label">Phase (°)</label>
|
| 42 |
+
<input type="range" min="0" max="360" value="0" step="15" class="slider" id="phase1">
|
| 43 |
+
<div class="slider-value" id="phase1Value">0°</div>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
|
| 47 |
+
<!-- Wave 2 -->
|
| 48 |
+
<div class="wave-section">
|
| 49 |
+
<div class="wave-header">
|
| 50 |
+
<label class="control-label">
|
| 51 |
+
<input type="checkbox" id="enableWave2" class="checkbox-input" checked>
|
| 52 |
+
Wave 2
|
| 53 |
+
</label>
|
| 54 |
+
<span class="wave-color" style="background-color: #d32f2f;"></span>
|
| 55 |
+
</div>
|
| 56 |
+
<div class="control-group">
|
| 57 |
+
<label for="freq2" class="control-label">Frequency (Hz)</label>
|
| 58 |
+
<input type="range" min="0" max="50" value="15" step="1" class="slider" id="freq2">
|
| 59 |
+
<div class="slider-value" id="freq2Value">15 Hz</div>
|
| 60 |
+
</div>
|
| 61 |
+
<div class="control-group">
|
| 62 |
+
<label for="amp2" class="control-label">Amplitude</label>
|
| 63 |
+
<input type="range" min="0" max="10" value="3" step="0.1" class="slider" id="amp2">
|
| 64 |
+
<div class="slider-value" id="amp2Value">3.0</div>
|
| 65 |
+
</div>
|
| 66 |
+
<div class="control-group">
|
| 67 |
+
<label for="phase2" class="control-label">Phase (°)</label>
|
| 68 |
+
<input type="range" min="0" max="360" value="90" step="15" class="slider" id="phase2">
|
| 69 |
+
<div class="slider-value" id="phase2Value">90°</div>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<!-- Wave 3 -->
|
| 74 |
+
<div class="wave-section">
|
| 75 |
+
<div class="wave-header">
|
| 76 |
+
<label class="control-label">
|
| 77 |
+
<input type="checkbox" id="enableWave3" class="checkbox-input">
|
| 78 |
+
Wave 3
|
| 79 |
+
</label>
|
| 80 |
+
<span class="wave-color" style="background-color: #388e3c;"></span>
|
| 81 |
+
</div>
|
| 82 |
+
<div class="control-group">
|
| 83 |
+
<label for="freq3" class="control-label">Frequency (Hz)</label>
|
| 84 |
+
<input type="range" min="0" max="50" value="25" step="1" class="slider" id="freq3">
|
| 85 |
+
<div class="slider-value" id="freq3Value">25 Hz</div>
|
| 86 |
+
</div>
|
| 87 |
+
<div class="control-group">
|
| 88 |
+
<label for="amp3" class="control-label">Amplitude</label>
|
| 89 |
+
<input type="range" min="0" max="10" value="0" step="0.1" class="slider" id="amp3">
|
| 90 |
+
<div class="slider-value" id="amp3Value">0.0</div>
|
| 91 |
+
</div>
|
| 92 |
+
<div class="control-group">
|
| 93 |
+
<label for="phase3" class="control-label">Phase (°)</label>
|
| 94 |
+
<input type="range" min="0" max="360" value="0" step="15" class="slider" id="phase3">
|
| 95 |
+
<div class="slider-value" id="phase3Value">0°</div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<!-- Wave 4 -->
|
| 100 |
+
<div class="wave-section">
|
| 101 |
+
<div class="wave-header">
|
| 102 |
+
<label class="control-label">
|
| 103 |
+
<input type="checkbox" id="enableWave4" class="checkbox-input">
|
| 104 |
+
Wave 4
|
| 105 |
+
</label>
|
| 106 |
+
<span class="wave-color" style="background-color: #f57c00;"></span>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="control-group">
|
| 109 |
+
<label for="freq4" class="control-label">Frequency (Hz)</label>
|
| 110 |
+
<input type="range" min="0" max="50" value="35" step="1" class="slider" id="freq4">
|
| 111 |
+
<div class="slider-value" id="freq4Value">35 Hz</div>
|
| 112 |
+
</div>
|
| 113 |
+
<div class="control-group">
|
| 114 |
+
<label for="amp4" class="control-label">Amplitude</label>
|
| 115 |
+
<input type="range" min="0" max="10" value="0" step="0.1" class="slider" id="amp4">
|
| 116 |
+
<div class="slider-value" id="amp4Value">0.0</div>
|
| 117 |
+
</div>
|
| 118 |
+
<div class="control-group">
|
| 119 |
+
<label for="phase4" class="control-label">Phase (°)</label>
|
| 120 |
+
<input type="range" min="0" max="360" value="0" step="15" class="slider" id="phase4">
|
| 121 |
+
<div class="slider-value" id="phase4Value">0°</div>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<!-- Global Settings -->
|
| 126 |
+
<div class="wave-section" style="border-top: 2px solid var(--color-border); padding-top: var(--space-md); margin-top: var(--space-md);">
|
| 127 |
+
<div class="control-group">
|
| 128 |
+
<label for="numSamples" class="control-label">Samples (N)</label>
|
| 129 |
+
<input type="range" min="64" max="512" value="256" step="1" class="slider" id="numSamples">
|
| 130 |
+
<div class="slider-value" id="numSamplesValue">256</div>
|
| 131 |
+
</div>
|
| 132 |
+
<div class="control-group">
|
| 133 |
+
<label class="control-label">
|
| 134 |
+
<input type="checkbox" id="addNoise" class="checkbox-input">
|
| 135 |
+
Add Gaussian Noise
|
| 136 |
+
</label>
|
| 137 |
+
</div>
|
| 138 |
+
<div class="control-group" id="noiseLevelGroup" style="opacity: 0.4;">
|
| 139 |
+
<label for="noiseLevel" class="control-label">Noise Level (σ)</label>
|
| 140 |
+
<input type="range" min="0.1" max="3" value="0.5" step="0.1" class="slider" id="noiseLevel" disabled>
|
| 141 |
+
<div class="slider-value" id="noiseLevelValue">0.5</div>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<div class="chart-hint" style="margin-top: 8px; font-size: 0.7rem; color: var(--color-text-muted);">
|
| 146 |
+
💡 Combine up to 4 sine waves to create complex signals
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
|
| 151 |
+
<div class="charts-container">
|
| 152 |
+
<!-- Time Domain Chart -->
|
| 153 |
+
<div class="chart-card" style="grid-column: span 2;">
|
| 154 |
+
<div class="chart-title">Time Domain Signal</div>
|
| 155 |
+
<div class="chart-container" style="height: 300px;">
|
| 156 |
+
<canvas id="timeDomainChart"></canvas>
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<!-- Magnitude Spectrum -->
|
| 161 |
+
<div class="chart-card">
|
| 162 |
+
<div class="chart-title">Magnitude Spectrum |X(f)|</div>
|
| 163 |
+
<div class="chart-container" style="height: 280px;">
|
| 164 |
+
<canvas id="magnitudeChart"></canvas>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
|
| 168 |
+
<!-- Phase Spectrum -->
|
| 169 |
+
<div class="chart-card" id="phaseCard">
|
| 170 |
+
<div class="chart-title">Phase Spectrum ∠X(f)</div>
|
| 171 |
+
<div class="chart-container" style="height: 280px;">
|
| 172 |
+
<canvas id="phaseChart"></canvas>
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
<!-- Signal Info -->
|
| 177 |
+
<div class="chart-card" style="grid-column: span 2;">
|
| 178 |
+
<div class="chart-title">Signal Information</div>
|
| 179 |
+
<div class="metrics-display" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-md);">
|
| 180 |
+
<div class="metric-item">
|
| 181 |
+
<span class="metric-label">Sampling Rate (Fs)</span>
|
| 182 |
+
<span class="metric-value" id="samplingRate">-</span>
|
| 183 |
+
</div>
|
| 184 |
+
<div class="metric-item">
|
| 185 |
+
<span class="metric-label">Nyquist Frequency</span>
|
| 186 |
+
<span class="metric-value" id="nyquistFreq">-</span>
|
| 187 |
+
</div>
|
| 188 |
+
<div class="metric-item">
|
| 189 |
+
<span class="metric-label">Frequency Resolution (Δf)</span>
|
| 190 |
+
<span class="metric-value" id="freqResolution">-</span>
|
| 191 |
+
</div>
|
| 192 |
+
<div class="metric-item">
|
| 193 |
+
<span class="metric-label">Signal Duration</span>
|
| 194 |
+
<span class="metric-value" id="signalDuration">-</span>
|
| 195 |
+
</div>
|
| 196 |
+
<div class="metric-item">
|
| 197 |
+
<span class="metric-label">Total Power</span>
|
| 198 |
+
<span class="metric-value" id="totalPower">-</span>
|
| 199 |
+
</div>
|
| 200 |
+
<div class="metric-item">
|
| 201 |
+
<span class="metric-label">RMS Amplitude</span>
|
| 202 |
+
<span class="metric-value" id="rmsAmplitude">-</span>
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
</div>
|
| 206 |
+
</div>
|
| 207 |
+
</div>
|
| 208 |
+
|
| 209 |
+
<!-- Footer -->
|
| 210 |
+
<footer>
|
| 211 |
+
<a href="https://github.com/berangerthomas/SchoolOfStatistics" target="_blank"
|
| 212 |
+
rel="noopener noreferrer">Project's GitHub</a>
|
| 213 |
+
</footer>
|
| 214 |
+
|
| 215 |
+
<script src="src/js/common.js"></script>
|
| 216 |
+
<script src="src/js/fourier_transform.js"></script>
|
| 217 |
+
</body>
|
| 218 |
+
|
| 219 |
+
</html>
|
inverse_classifier.html
CHANGED
|
@@ -4,17 +4,17 @@
|
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
-
<title>
|
| 8 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
| 9 |
-
<link rel="stylesheet" href="src/css/
|
| 10 |
</head>
|
| 11 |
|
| 12 |
<body>
|
| 13 |
<div class="container">
|
| 14 |
-
<h1>
|
| 15 |
|
| 16 |
<div class="floating-controls" id="floatingControls">
|
| 17 |
-
<div class="controls-title" id="controlsTitle"
|
| 18 |
<div class="matrix-grid">
|
| 19 |
<div class="control-group">
|
| 20 |
<div class="control-label">False Negatives (FN) <span class="lock-toggle" data-param="fn">🔓</span>
|
|
@@ -45,15 +45,15 @@
|
|
| 45 |
|
| 46 |
<div class="charts-container">
|
| 47 |
<div class="chart-card">
|
| 48 |
-
<div class="chart-title"
|
| 49 |
<div class="chart-container"><canvas id="rocChart"></canvas></div>
|
| 50 |
</div>
|
| 51 |
<div class="chart-card">
|
| 52 |
-
<div class="chart-title"
|
| 53 |
<div class="chart-container"><canvas id="metricsChart"></canvas></div>
|
| 54 |
</div>
|
| 55 |
<div class="chart-card">
|
| 56 |
-
<div class="chart-title"
|
| 57 |
<div class="confusion-matrix-layout">
|
| 58 |
<div class="matrix-ylabel">TRUE class</div>
|
| 59 |
<div class="matrix-canvas-container">
|
|
@@ -63,7 +63,7 @@
|
|
| 63 |
</div>
|
| 64 |
</div>
|
| 65 |
<div class="chart-card">
|
| 66 |
-
<div class="chart-title"
|
| 67 |
<div class="chart-container"><canvas id="scoresChart"></canvas></div>
|
| 68 |
</div>
|
| 69 |
</div>
|
|
@@ -75,6 +75,7 @@
|
|
| 75 |
rel="noopener noreferrer">Project's GitHub</a>
|
| 76 |
</footer>
|
| 77 |
|
|
|
|
| 78 |
<script src="src/js/inverse_classifier.js"></script>
|
| 79 |
</body>
|
| 80 |
|
|
|
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Inverse Classification Dashboard</title>
|
| 8 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
| 9 |
+
<link rel="stylesheet" href="src/css/style.css">
|
| 10 |
</head>
|
| 11 |
|
| 12 |
<body>
|
| 13 |
<div class="container">
|
| 14 |
+
<h1>Inverse Classification Dashboard</h1>
|
| 15 |
|
| 16 |
<div class="floating-controls" id="floatingControls">
|
| 17 |
+
<div class="controls-title" id="controlsTitle">Confusion Matrix Controls</div>
|
| 18 |
<div class="matrix-grid">
|
| 19 |
<div class="control-group">
|
| 20 |
<div class="control-label">False Negatives (FN) <span class="lock-toggle" data-param="fn">🔓</span>
|
|
|
|
| 45 |
|
| 46 |
<div class="charts-container">
|
| 47 |
<div class="chart-card">
|
| 48 |
+
<div class="chart-title">ROC Curve & AUC</div>
|
| 49 |
<div class="chart-container"><canvas id="rocChart"></canvas></div>
|
| 50 |
</div>
|
| 51 |
<div class="chart-card">
|
| 52 |
+
<div class="chart-title">Performance Metrics</div>
|
| 53 |
<div class="chart-container"><canvas id="metricsChart"></canvas></div>
|
| 54 |
</div>
|
| 55 |
<div class="chart-card">
|
| 56 |
+
<div class="chart-title">Confusion Matrix</div>
|
| 57 |
<div class="confusion-matrix-layout">
|
| 58 |
<div class="matrix-ylabel">TRUE class</div>
|
| 59 |
<div class="matrix-canvas-container">
|
|
|
|
| 63 |
</div>
|
| 64 |
</div>
|
| 65 |
<div class="chart-card">
|
| 66 |
+
<div class="chart-title">Classifier Scores Distribution</div>
|
| 67 |
<div class="chart-container"><canvas id="scoresChart"></canvas></div>
|
| 68 |
</div>
|
| 69 |
</div>
|
|
|
|
| 75 |
rel="noopener noreferrer">Project's GitHub</a>
|
| 76 |
</footer>
|
| 77 |
|
| 78 |
+
<script src="src/js/common.js"></script>
|
| 79 |
<script src="src/js/inverse_classifier.js"></script>
|
| 80 |
</body>
|
| 81 |
|
linear_regression.html
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Linear Regression Playground</title>
|
| 8 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
| 9 |
+
<link rel="stylesheet" href="src/css/style.css">
|
| 10 |
+
<link rel="stylesheet" href="src/css/linear_regression.css">
|
| 11 |
+
</head>
|
| 12 |
+
|
| 13 |
+
<body>
|
| 14 |
+
<div class="container">
|
| 15 |
+
<h1>Linear Regression Playground</h1>
|
| 16 |
+
|
| 17 |
+
<div class="floating-controls" id="floatingControls">
|
| 18 |
+
<div class="controls-title" id="controlsTitle">Regression Controls</div>
|
| 19 |
+
<div class="controls-grid">
|
| 20 |
+
<div class="control-group">
|
| 21 |
+
<label for="degreeSlider" class="control-label">Polynomial Degree</label>
|
| 22 |
+
<input type="range" min="1" max="10" value="1" step="1" class="slider" id="degreeSlider">
|
| 23 |
+
<div class="slider-value" id="degreeValue">1</div>
|
| 24 |
+
</div>
|
| 25 |
+
<div class="control-group">
|
| 26 |
+
<label class="control-label">
|
| 27 |
+
<input type="checkbox" id="showResiduals" class="checkbox-input">
|
| 28 |
+
Show Residuals
|
| 29 |
+
</label>
|
| 30 |
+
</div>
|
| 31 |
+
<div class="control-group">
|
| 32 |
+
<label class="control-label">
|
| 33 |
+
<input type="checkbox" id="showConfidence" class="checkbox-input">
|
| 34 |
+
Show Confidence Band (±2σ)
|
| 35 |
+
</label>
|
| 36 |
+
</div>
|
| 37 |
+
<div class="control-group">
|
| 38 |
+
<button id="clearPointsBtn" class="button-start">Clear All Points</button>
|
| 39 |
+
</div>
|
| 40 |
+
<div class="control-group">
|
| 41 |
+
<button id="addRandomBtn" class="button-start" style="background-color: var(--color-accent);">Add 5 Random Points</button>
|
| 42 |
+
</div>
|
| 43 |
+
<div class="control-group">
|
| 44 |
+
<button id="resetZoomBtn" class="button-start" style="background-color: #757575;">↺ Reset Zoom</button>
|
| 45 |
+
</div>
|
| 46 |
+
<div class="chart-hint" style="margin-top: 8px; font-size: 0.7rem; color: var(--color-text-muted);">
|
| 47 |
+
💡 Roulette souris : zoomer/dézoomer
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
<div class="chart-hint" style="margin-top: 16px; font-size: 0.75rem;">
|
| 51 |
+
<strong>How to use:</strong><br>
|
| 52 |
+
• Click on the main chart to add points<br>
|
| 53 |
+
• Right-click a point to remove it<br>
|
| 54 |
+
• Drag a point to move it
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<div class="charts-container">
|
| 59 |
+
<div class="chart-card" style="grid-column: span 2;">
|
| 60 |
+
<div class="chart-title">Data & Regression Fit</div>
|
| 61 |
+
<div class="chart-container" style="height: 350px;">
|
| 62 |
+
<canvas id="mainChart"></canvas>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
<div class="chart-card">
|
| 66 |
+
<div class="chart-title">Residual Plot</div>
|
| 67 |
+
<div class="chart-container">
|
| 68 |
+
<canvas id="residualChart"></canvas>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
<div class="chart-card">
|
| 72 |
+
<div class="chart-title">Statistics</div>
|
| 73 |
+
<div class="metrics-display">
|
| 74 |
+
<div class="metric-item" title="Proportion of variance explained by the model">
|
| 75 |
+
<span class="metric-label">R² (Coefficient of Determination)</span>
|
| 76 |
+
<span class="metric-value" id="r2Value">-</span>
|
| 77 |
+
</div>
|
| 78 |
+
<div class="metric-item" title="R² adjusted for the number of predictors">
|
| 79 |
+
<span class="metric-label">Adjusted R²</span>
|
| 80 |
+
<span class="metric-value" id="adjustedR2Value">-</span>
|
| 81 |
+
</div>
|
| 82 |
+
<div class="metric-item" title="Mean Squared Error">
|
| 83 |
+
<span class="metric-label">MSE</span>
|
| 84 |
+
<span class="metric-value" id="mseValue">-</span>
|
| 85 |
+
</div>
|
| 86 |
+
<div class="metric-item" title="Mean Absolute Error">
|
| 87 |
+
<span class="metric-label">MAE</span>
|
| 88 |
+
<span class="metric-value" id="maeValue">-</span>
|
| 89 |
+
</div>
|
| 90 |
+
<div class="metric-item" title="Number of data points">
|
| 91 |
+
<span class="metric-label">Number of Points</span>
|
| 92 |
+
<span class="metric-value" id="nPointsValue">-</span>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="metric-item" title="Root Mean Squared Error">
|
| 95 |
+
<span class="metric-label">RMSE</span>
|
| 96 |
+
<span class="metric-value" id="rmseValue">-</span>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<!-- Footer -->
|
| 104 |
+
<footer>
|
| 105 |
+
<a href="https://github.com/berangerthomas/SchoolOfStatistics" target="_blank"
|
| 106 |
+
rel="noopener noreferrer">Project's GitHub</a>
|
| 107 |
+
</footer>
|
| 108 |
+
|
| 109 |
+
<script src="src/js/common.js"></script>
|
| 110 |
+
<script src="src/js/linear_regression.js"></script>
|
| 111 |
+
</body>
|
| 112 |
+
|
| 113 |
+
</html>
|
src/css/fourier_transform.css
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================
|
| 2 |
+
Fourier Transform Visualizer - Specific Styles
|
| 3 |
+
============================================================ */
|
| 4 |
+
|
| 5 |
+
/* Wave Section Styling */
|
| 6 |
+
.wave-section {
|
| 7 |
+
background: var(--color-surface-alt);
|
| 8 |
+
border-radius: var(--radius-md);
|
| 9 |
+
padding: var(--space-md);
|
| 10 |
+
margin-bottom: var(--space-md);
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
.wave-section:last-child {
|
| 14 |
+
margin-bottom: 0;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.wave-header {
|
| 18 |
+
display: flex;
|
| 19 |
+
justify-content: space-between;
|
| 20 |
+
align-items: center;
|
| 21 |
+
margin-bottom: var(--space-sm);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.wave-header .control-label {
|
| 25 |
+
margin-bottom: 0;
|
| 26 |
+
font-weight: 600;
|
| 27 |
+
font-size: 0.875rem;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.wave-color {
|
| 31 |
+
width: 16px;
|
| 32 |
+
height: 16px;
|
| 33 |
+
border-radius: 50%;
|
| 34 |
+
display: inline-block;
|
| 35 |
+
border: 2px solid var(--color-surface);
|
| 36 |
+
box-shadow: var(--shadow-sm);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/* Checkbox input styling */
|
| 40 |
+
.checkbox-input {
|
| 41 |
+
margin-right: var(--space-sm);
|
| 42 |
+
width: 16px;
|
| 43 |
+
height: 16px;
|
| 44 |
+
cursor: pointer;
|
| 45 |
+
accent-color: var(--color-accent);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/* Disabled wave section styling */
|
| 49 |
+
.wave-section.disabled {
|
| 50 |
+
opacity: 0.5;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.wave-section.disabled .slider {
|
| 54 |
+
pointer-events: none;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/* Control group spacing in wave sections */
|
| 58 |
+
.wave-section .control-group {
|
| 59 |
+
margin-bottom: var(--space-sm);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.wave-section .control-group:last-child {
|
| 63 |
+
margin-bottom: 0;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/* Phase spectrum card transition */
|
| 67 |
+
#phaseCard {
|
| 68 |
+
transition: opacity var(--transition-normal);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* Metrics display enhancements */
|
| 72 |
+
.metrics-display .metric-item {
|
| 73 |
+
padding: var(--space-sm) var(--space-md);
|
| 74 |
+
background: var(--color-surface);
|
| 75 |
+
border-radius: var(--radius-sm);
|
| 76 |
+
border: 1px solid var(--color-border);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/* Frequency value styling */
|
| 80 |
+
#freq1Value, #freq2Value, #freq3Value, #freq4Value {
|
| 81 |
+
color: var(--color-accent);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* Responsive adjustments */
|
| 85 |
+
@media (max-width: 1200px) {
|
| 86 |
+
.metrics-display {
|
| 87 |
+
grid-template-columns: 1fr !important;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
@media (max-width: 600px) {
|
| 92 |
+
.wave-section {
|
| 93 |
+
padding: var(--space-sm);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.metrics-display .metric-item {
|
| 97 |
+
flex-direction: column;
|
| 98 |
+
align-items: flex-start;
|
| 99 |
+
gap: var(--space-xs);
|
| 100 |
+
}
|
| 101 |
+
}
|
src/css/inverse_style.css
DELETED
|
@@ -1,237 +0,0 @@
|
|
| 1 |
-
* {
|
| 2 |
-
margin: 0;
|
| 3 |
-
padding: 0;
|
| 4 |
-
box-sizing: border-box;
|
| 5 |
-
}
|
| 6 |
-
|
| 7 |
-
body {
|
| 8 |
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 9 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 10 |
-
min-height: 100vh;
|
| 11 |
-
padding: 20px;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
.container {
|
| 15 |
-
max-width: 1400px;
|
| 16 |
-
margin: 0 auto;
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
h1 {
|
| 20 |
-
text-align: center;
|
| 21 |
-
color: white;
|
| 22 |
-
margin-bottom: 30px;
|
| 23 |
-
font-size: 2.5em;
|
| 24 |
-
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
.floating-controls {
|
| 28 |
-
position: fixed;
|
| 29 |
-
top: 20px;
|
| 30 |
-
right: 20px;
|
| 31 |
-
background: rgba(255, 255, 255, 0.65);
|
| 32 |
-
backdrop-filter: blur(20px);
|
| 33 |
-
border-radius: 15px;
|
| 34 |
-
padding: 20px;
|
| 35 |
-
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
| 36 |
-
border: 1px solid rgba(255, 255, 255, 0.3);
|
| 37 |
-
z-index: 1000;
|
| 38 |
-
width: 320px;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
.controls-title {
|
| 42 |
-
text-align: center;
|
| 43 |
-
font-size: 18px;
|
| 44 |
-
font-weight: bold;
|
| 45 |
-
margin-bottom: 15px;
|
| 46 |
-
color: #333;
|
| 47 |
-
cursor: grab;
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
.controls-title:active {
|
| 51 |
-
cursor: grabbing;
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
.matrix-grid {
|
| 55 |
-
display: grid;
|
| 56 |
-
grid-template-columns: 1fr 1fr;
|
| 57 |
-
gap: 15px;
|
| 58 |
-
margin-bottom: 15px;
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
.control-group {
|
| 62 |
-
text-align: center;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
.control-label {
|
| 66 |
-
font-weight: bold;
|
| 67 |
-
margin-bottom: 5px;
|
| 68 |
-
font-size: 14px;
|
| 69 |
-
color: #555;
|
| 70 |
-
display: flex;
|
| 71 |
-
justify-content: center;
|
| 72 |
-
align-items: center;
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
.lock-toggle {
|
| 76 |
-
cursor: pointer;
|
| 77 |
-
margin-left: 8px;
|
| 78 |
-
font-size: 16px;
|
| 79 |
-
transition: color 0.2s;
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
.lock-toggle.locked {
|
| 83 |
-
color: #d32f2f;
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
.slider-container {
|
| 87 |
-
position: relative;
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
.slider {
|
| 91 |
-
width: 100%;
|
| 92 |
-
height: 30px;
|
| 93 |
-
border-radius: 15px;
|
| 94 |
-
background: #ddd;
|
| 95 |
-
outline: none;
|
| 96 |
-
-webkit-appearance: none;
|
| 97 |
-
appearance: none;
|
| 98 |
-
transition: opacity 0.2s;
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
.slider:disabled {
|
| 102 |
-
opacity: 0.5;
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
.slider:disabled::-webkit-slider-thumb {
|
| 106 |
-
background: #9E9E9E;
|
| 107 |
-
cursor: not-allowed;
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
.slider:disabled::-moz-range-thumb {
|
| 111 |
-
background: #9E9E9E;
|
| 112 |
-
cursor: not-allowed;
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
.slider::-webkit-slider-thumb {
|
| 116 |
-
-webkit-appearance: none;
|
| 117 |
-
appearance: none;
|
| 118 |
-
width: 20px;
|
| 119 |
-
height: 20px;
|
| 120 |
-
border-radius: 50%;
|
| 121 |
-
background: #4CAF50;
|
| 122 |
-
cursor: pointer;
|
| 123 |
-
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
.slider::-moz-range-thumb {
|
| 127 |
-
width: 20px;
|
| 128 |
-
height: 20px;
|
| 129 |
-
border-radius: 50%;
|
| 130 |
-
background: #4CAF50;
|
| 131 |
-
cursor: pointer;
|
| 132 |
-
border: none;
|
| 133 |
-
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
.slider-value {
|
| 137 |
-
font-weight: bold;
|
| 138 |
-
color: #333;
|
| 139 |
-
margin-top: 5px;
|
| 140 |
-
font-size: 16px;
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
.charts-container {
|
| 144 |
-
display: grid;
|
| 145 |
-
grid-template-columns: 1fr 1fr;
|
| 146 |
-
gap: 30px;
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
.chart-card {
|
| 150 |
-
background: rgba(255, 255, 255, 0.95);
|
| 151 |
-
border-radius: 15px;
|
| 152 |
-
padding: 25px;
|
| 153 |
-
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
.chart-title {
|
| 157 |
-
font-size: 18px;
|
| 158 |
-
font-weight: bold;
|
| 159 |
-
margin-bottom: 15px;
|
| 160 |
-
text-align: center;
|
| 161 |
-
color: #333;
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
.chart-container {
|
| 165 |
-
position: relative;
|
| 166 |
-
height: 300px;
|
| 167 |
-
margin-bottom: 20px;
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
.confusion-matrix-layout {
|
| 171 |
-
position: relative;
|
| 172 |
-
width: 250px;
|
| 173 |
-
height: 250px;
|
| 174 |
-
margin: 0 auto; /* Center the block horizontally */
|
| 175 |
-
top: 50%; /* Center the block vertically */
|
| 176 |
-
transform: translateY(-50%);
|
| 177 |
-
}
|
| 178 |
-
|
| 179 |
-
.matrix-ylabel,
|
| 180 |
-
.matrix-xlabel {
|
| 181 |
-
position: absolute;
|
| 182 |
-
font-weight: bold;
|
| 183 |
-
color: #555;
|
| 184 |
-
font-style: italic;
|
| 185 |
-
white-space: nowrap;
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
.matrix-ylabel {
|
| 189 |
-
top: 50%;
|
| 190 |
-
left: -20px; /* Position label just outside the container */
|
| 191 |
-
transform: translateY(-50%) rotate(-90deg);
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
.matrix-xlabel {
|
| 195 |
-
bottom: -25px; /* Position label just outside the container */
|
| 196 |
-
left: 50%;
|
| 197 |
-
transform: translateX(-50%);
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
.matrix-canvas-container {
|
| 201 |
-
width: 100%;
|
| 202 |
-
height: 100%;
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
#matrixChart {
|
| 206 |
-
max-width: 100%;
|
| 207 |
-
max-height: 100%;
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
footer {
|
| 211 |
-
text-align: center;
|
| 212 |
-
padding: 20px;
|
| 213 |
-
margin-top: 40px;
|
| 214 |
-
}
|
| 215 |
-
|
| 216 |
-
footer a {
|
| 217 |
-
color: rgba(255, 255, 255, 0.8);
|
| 218 |
-
text-decoration: none;
|
| 219 |
-
font-weight: bold;
|
| 220 |
-
transition: color 0.3s;
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
footer a:hover {
|
| 224 |
-
color: white;
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
@media (max-width: 1200px) {
|
| 228 |
-
.charts-container {
|
| 229 |
-
grid-template-columns: 1fr;
|
| 230 |
-
}
|
| 231 |
-
|
| 232 |
-
.floating-controls {
|
| 233 |
-
position: static;
|
| 234 |
-
width: 100%;
|
| 235 |
-
margin-bottom: 20px;
|
| 236 |
-
}
|
| 237 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/css/linear_regression.css
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================
|
| 2 |
+
Linear Regression Playground - Specific Styles
|
| 3 |
+
============================================================ */
|
| 4 |
+
|
| 5 |
+
/* Main chart takes full width */
|
| 6 |
+
#mainChart {
|
| 7 |
+
height: 100% !important;
|
| 8 |
+
width: 100% !important;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
/* Custom checkbox styling */
|
| 12 |
+
.control-group label.control-label:has(input[type="checkbox"]) {
|
| 13 |
+
display: flex;
|
| 14 |
+
align-items: center;
|
| 15 |
+
cursor: pointer;
|
| 16 |
+
padding: var(--space-sm);
|
| 17 |
+
border-radius: var(--radius-sm);
|
| 18 |
+
transition: background-color var(--transition-fast);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.control-group label.control-label:has(input[type="checkbox"]):hover {
|
| 22 |
+
background-color: var(--color-surface-alt);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
.checkbox-input {
|
| 26 |
+
margin-right: var(--space-sm);
|
| 27 |
+
width: 16px;
|
| 28 |
+
height: 16px;
|
| 29 |
+
cursor: pointer;
|
| 30 |
+
accent-color: var(--color-accent);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/* Stats panel styling */
|
| 34 |
+
.metrics-display {
|
| 35 |
+
padding: var(--space-md);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.metric-item {
|
| 39 |
+
display: flex;
|
| 40 |
+
justify-content: space-between;
|
| 41 |
+
align-items: center;
|
| 42 |
+
padding: var(--space-sm) 0;
|
| 43 |
+
border-bottom: 1px solid var(--color-border);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.metric-item:last-child {
|
| 47 |
+
border-bottom: none;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.metric-label {
|
| 51 |
+
font-size: 0.8125rem;
|
| 52 |
+
color: var(--color-text-secondary);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.metric-value {
|
| 56 |
+
font-family: 'SF Mono', Monaco, monospace;
|
| 57 |
+
font-size: 0.875rem;
|
| 58 |
+
font-weight: 600;
|
| 59 |
+
color: var(--color-text-primary);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/* Dragging state for points */
|
| 63 |
+
.canvas-container {
|
| 64 |
+
position: relative;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/* Button variations */
|
| 68 |
+
.button-start {
|
| 69 |
+
width: 100%;
|
| 70 |
+
margin: var(--space-xs) 0;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* Hint box styling */
|
| 74 |
+
.chart-hint {
|
| 75 |
+
background: var(--color-accent-light);
|
| 76 |
+
border-left: 3px solid var(--color-accent);
|
| 77 |
+
padding: var(--space-sm) var(--space-md);
|
| 78 |
+
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
| 79 |
+
font-size: 0.75rem;
|
| 80 |
+
color: var(--color-text-secondary);
|
| 81 |
+
line-height: 1.5;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.chart-hint strong {
|
| 85 |
+
color: var(--color-text-primary);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/* Canvas cursor states */
|
| 89 |
+
#mainChart {
|
| 90 |
+
cursor: crosshair;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
#mainChart.dragging {
|
| 94 |
+
cursor: grabbing;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
#mainChart.hover-point {
|
| 98 |
+
cursor: grab;
|
| 99 |
+
}
|
src/css/style.css
CHANGED
|
@@ -1,49 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
* {
|
| 2 |
margin: 0;
|
| 3 |
padding: 0;
|
| 4 |
box-sizing: border-box;
|
| 5 |
}
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
body {
|
| 8 |
-
font-family: 'Segoe UI',
|
| 9 |
-
background:
|
|
|
|
|
|
|
| 10 |
min-height: 100vh;
|
| 11 |
-
padding: 20px;
|
| 12 |
}
|
| 13 |
|
|
|
|
| 14 |
.container {
|
| 15 |
max-width: 1400px;
|
| 16 |
margin: 0 auto;
|
|
|
|
| 17 |
}
|
| 18 |
|
|
|
|
| 19 |
h1 {
|
| 20 |
text-align: center;
|
| 21 |
-
color:
|
| 22 |
-
margin-bottom:
|
| 23 |
-
font-size:
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
|
|
|
|
| 27 |
.floating-controls {
|
| 28 |
position: fixed;
|
| 29 |
-
top:
|
| 30 |
-
right:
|
| 31 |
-
background:
|
| 32 |
-
|
| 33 |
-
border-radius:
|
| 34 |
-
padding:
|
| 35 |
-
box-shadow:
|
| 36 |
-
border: 1px solid rgba(255, 255, 255, 0.3);
|
| 37 |
z-index: 1000;
|
| 38 |
-
width:
|
|
|
|
|
|
|
| 39 |
}
|
| 40 |
|
| 41 |
.controls-title {
|
| 42 |
-
|
| 43 |
-
font-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
| 47 |
cursor: grab;
|
| 48 |
}
|
| 49 |
|
|
@@ -54,84 +117,195 @@ h1 {
|
|
| 54 |
.controls-grid {
|
| 55 |
display: grid;
|
| 56 |
grid-template-columns: 1fr;
|
| 57 |
-
gap:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
|
|
|
|
| 60 |
.control-group {
|
| 61 |
-
text-align:
|
| 62 |
}
|
| 63 |
|
| 64 |
.control-label {
|
| 65 |
-
font-weight:
|
| 66 |
-
margin-bottom:
|
| 67 |
-
font-size:
|
| 68 |
-
color:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
}
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
.slider {
|
| 72 |
width: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
|
| 75 |
.slider-value {
|
| 76 |
-
font-weight:
|
| 77 |
-
color:
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
}
|
| 81 |
|
|
|
|
| 82 |
.charts-container {
|
| 83 |
display: grid;
|
| 84 |
-
grid-template-columns:
|
| 85 |
-
gap:
|
|
|
|
| 86 |
}
|
| 87 |
|
|
|
|
| 88 |
.chart-card {
|
| 89 |
-
background:
|
| 90 |
-
border
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
|
| 95 |
.chart-title {
|
| 96 |
-
font-size:
|
| 97 |
-
font-weight:
|
| 98 |
-
margin-bottom:
|
| 99 |
text-align: center;
|
| 100 |
-
color:
|
|
|
|
|
|
|
| 101 |
}
|
| 102 |
|
| 103 |
.chart-container {
|
| 104 |
position: relative;
|
| 105 |
-
height:
|
| 106 |
-
margin-bottom: 20px;
|
| 107 |
}
|
| 108 |
|
|
|
|
| 109 |
.confusion-matrix-layout {
|
| 110 |
position: relative;
|
| 111 |
width: 250px;
|
| 112 |
height: 250px;
|
| 113 |
-
margin: 0 auto;
|
| 114 |
-
top: 50%;
|
| 115 |
transform: translateY(-50%);
|
| 116 |
}
|
| 117 |
|
| 118 |
.matrix-ylabel,
|
| 119 |
.matrix-xlabel {
|
| 120 |
position: absolute;
|
| 121 |
-
font-weight:
|
| 122 |
-
color:
|
| 123 |
-
font-
|
|
|
|
|
|
|
| 124 |
white-space: nowrap;
|
| 125 |
}
|
| 126 |
|
| 127 |
.matrix-ylabel {
|
| 128 |
top: 50%;
|
| 129 |
-
left: -
|
| 130 |
transform: translateY(-50%) rotate(-90deg);
|
| 131 |
}
|
| 132 |
|
| 133 |
.matrix-xlabel {
|
| 134 |
-
bottom: -
|
| 135 |
left: 50%;
|
| 136 |
transform: translateX(-50%);
|
| 137 |
}
|
|
@@ -146,32 +320,194 @@ h1 {
|
|
| 146 |
max-height: 100%;
|
| 147 |
}
|
| 148 |
|
| 149 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
footer {
|
| 151 |
text-align: center;
|
| 152 |
-
padding:
|
| 153 |
-
margin-top:
|
|
|
|
|
|
|
| 154 |
}
|
| 155 |
|
| 156 |
footer a {
|
| 157 |
-
color:
|
| 158 |
text-decoration: none;
|
| 159 |
-
font-
|
| 160 |
-
|
|
|
|
| 161 |
}
|
| 162 |
|
| 163 |
footer a:hover {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
color: white;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
}
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
@media (max-width: 1200px) {
|
| 168 |
.charts-container {
|
| 169 |
grid-template-columns: 1fr;
|
|
|
|
| 170 |
}
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
.floating-controls {
|
| 173 |
position: static;
|
| 174 |
width: 100%;
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
}
|
| 177 |
}
|
|
|
|
| 1 |
+
/* ============================================================
|
| 2 |
+
School of Statistics - Design System
|
| 3 |
+
Professional, Clean, Unified Styles
|
| 4 |
+
============================================================ */
|
| 5 |
+
|
| 6 |
+
/* CSS Variables for consistent theming */
|
| 7 |
+
:root {
|
| 8 |
+
/* Colors */
|
| 9 |
+
--color-bg: #f5f5f5;
|
| 10 |
+
--color-surface: #ffffff;
|
| 11 |
+
--color-surface-alt: #fafafa;
|
| 12 |
+
--color-border: #e0e0e0;
|
| 13 |
+
--color-text-primary: #212121;
|
| 14 |
+
--color-text-secondary: #616161;
|
| 15 |
+
--color-text-muted: #9e9e9e;
|
| 16 |
+
--color-accent: #1976d2;
|
| 17 |
+
--color-accent-light: #e3f2fd;
|
| 18 |
+
--color-success: #2e7d32;
|
| 19 |
+
--color-warning: #ed6c02;
|
| 20 |
+
--color-error: #d32f2f;
|
| 21 |
+
|
| 22 |
+
/* Spacing */
|
| 23 |
+
--space-xs: 4px;
|
| 24 |
+
--space-sm: 8px;
|
| 25 |
+
--space-md: 16px;
|
| 26 |
+
--space-lg: 24px;
|
| 27 |
+
--space-xl: 32px;
|
| 28 |
+
|
| 29 |
+
/* Border radius */
|
| 30 |
+
--radius-sm: 2px;
|
| 31 |
+
--radius-md: 4px;
|
| 32 |
+
|
| 33 |
+
/* Shadows */
|
| 34 |
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
|
| 35 |
+
--shadow-md: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 36 |
+
|
| 37 |
+
/* Transitions */
|
| 38 |
+
--transition-fast: 0.15s ease;
|
| 39 |
+
--transition-normal: 0.2s ease;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* Reset & Base */
|
| 43 |
* {
|
| 44 |
margin: 0;
|
| 45 |
padding: 0;
|
| 46 |
box-sizing: border-box;
|
| 47 |
}
|
| 48 |
|
| 49 |
+
html {
|
| 50 |
+
font-size: 16px;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
body {
|
| 54 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
| 55 |
+
background-color: var(--color-bg);
|
| 56 |
+
color: var(--color-text-primary);
|
| 57 |
+
line-height: 1.5;
|
| 58 |
min-height: 100vh;
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
+
/* Layout Container */
|
| 62 |
.container {
|
| 63 |
max-width: 1400px;
|
| 64 |
margin: 0 auto;
|
| 65 |
+
padding: var(--space-lg);
|
| 66 |
}
|
| 67 |
|
| 68 |
+
/* Header */
|
| 69 |
h1 {
|
| 70 |
text-align: center;
|
| 71 |
+
color: var(--color-text-primary);
|
| 72 |
+
margin-bottom: var(--space-xl);
|
| 73 |
+
font-size: 1.75rem;
|
| 74 |
+
font-weight: 600;
|
| 75 |
+
letter-spacing: -0.02em;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.subtitle {
|
| 79 |
+
text-align: center;
|
| 80 |
+
color: var(--color-text-secondary);
|
| 81 |
+
margin-bottom: var(--space-xl);
|
| 82 |
+
font-size: 0.95rem;
|
| 83 |
}
|
| 84 |
|
| 85 |
+
/* Floating Controls Panel */
|
| 86 |
.floating-controls {
|
| 87 |
position: fixed;
|
| 88 |
+
top: var(--space-lg);
|
| 89 |
+
right: var(--space-lg);
|
| 90 |
+
background: var(--color-surface);
|
| 91 |
+
border: 1px solid var(--color-border);
|
| 92 |
+
border-radius: var(--radius-md);
|
| 93 |
+
padding: var(--space-lg);
|
| 94 |
+
box-shadow: var(--shadow-md);
|
|
|
|
| 95 |
z-index: 1000;
|
| 96 |
+
width: 300px;
|
| 97 |
+
max-height: calc(100vh - var(--space-lg) * 2);
|
| 98 |
+
overflow-y: auto;
|
| 99 |
}
|
| 100 |
|
| 101 |
.controls-title {
|
| 102 |
+
font-size: 0.875rem;
|
| 103 |
+
font-weight: 600;
|
| 104 |
+
margin-bottom: var(--space-md);
|
| 105 |
+
color: var(--color-text-primary);
|
| 106 |
+
text-transform: uppercase;
|
| 107 |
+
letter-spacing: 0.05em;
|
| 108 |
+
border-bottom: 1px solid var(--color-border);
|
| 109 |
+
padding-bottom: var(--space-sm);
|
| 110 |
cursor: grab;
|
| 111 |
}
|
| 112 |
|
|
|
|
| 117 |
.controls-grid {
|
| 118 |
display: grid;
|
| 119 |
grid-template-columns: 1fr;
|
| 120 |
+
gap: var(--space-md);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.matrix-grid {
|
| 124 |
+
display: grid;
|
| 125 |
+
grid-template-columns: 1fr 1fr;
|
| 126 |
+
gap: var(--space-md);
|
| 127 |
}
|
| 128 |
|
| 129 |
+
/* Control Groups */
|
| 130 |
.control-group {
|
| 131 |
+
text-align: left;
|
| 132 |
}
|
| 133 |
|
| 134 |
.control-label {
|
| 135 |
+
font-weight: 500;
|
| 136 |
+
margin-bottom: var(--space-xs);
|
| 137 |
+
font-size: 0.8125rem;
|
| 138 |
+
color: var(--color-text-secondary);
|
| 139 |
+
display: flex;
|
| 140 |
+
justify-content: flex-start;
|
| 141 |
+
align-items: center;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
/* Lock Toggle */
|
| 145 |
+
.lock-toggle {
|
| 146 |
+
cursor: pointer;
|
| 147 |
+
margin-left: var(--space-sm);
|
| 148 |
+
font-size: 0.875rem;
|
| 149 |
+
opacity: 0.6;
|
| 150 |
+
transition: opacity var(--transition-fast);
|
| 151 |
}
|
| 152 |
|
| 153 |
+
.lock-toggle:hover {
|
| 154 |
+
opacity: 1;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.lock-toggle.locked {
|
| 158 |
+
opacity: 1;
|
| 159 |
+
color: var(--color-error);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/* Sliders */
|
| 163 |
.slider {
|
| 164 |
width: 100%;
|
| 165 |
+
height: 4px;
|
| 166 |
+
border-radius: var(--radius-sm);
|
| 167 |
+
background: var(--color-border);
|
| 168 |
+
outline: none;
|
| 169 |
+
-webkit-appearance: none;
|
| 170 |
+
appearance: none;
|
| 171 |
+
margin: var(--space-sm) 0;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.slider:disabled {
|
| 175 |
+
opacity: 0.4;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.slider::-webkit-slider-thumb {
|
| 179 |
+
-webkit-appearance: none;
|
| 180 |
+
appearance: none;
|
| 181 |
+
width: 16px;
|
| 182 |
+
height: 16px;
|
| 183 |
+
border-radius: 50%;
|
| 184 |
+
background: var(--color-accent);
|
| 185 |
+
cursor: pointer;
|
| 186 |
+
border: 2px solid var(--color-surface);
|
| 187 |
+
box-shadow: var(--shadow-sm);
|
| 188 |
+
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.slider::-webkit-slider-thumb:hover {
|
| 192 |
+
transform: scale(1.1);
|
| 193 |
+
box-shadow: 0 0 0 3px var(--color-accent-light);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.slider:disabled::-webkit-slider-thumb {
|
| 197 |
+
background: var(--color-text-muted);
|
| 198 |
+
cursor: not-allowed;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.slider::-moz-range-thumb {
|
| 202 |
+
width: 16px;
|
| 203 |
+
height: 16px;
|
| 204 |
+
border-radius: 50%;
|
| 205 |
+
background: var(--color-accent);
|
| 206 |
+
cursor: pointer;
|
| 207 |
+
border: 2px solid var(--color-surface);
|
| 208 |
+
box-shadow: var(--shadow-sm);
|
| 209 |
}
|
| 210 |
|
| 211 |
.slider-value {
|
| 212 |
+
font-weight: 500;
|
| 213 |
+
color: var(--color-text-primary);
|
| 214 |
+
font-size: 0.8125rem;
|
| 215 |
+
text-align: right;
|
| 216 |
+
font-family: 'SF Mono', Monaco, monospace;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
/* Checkbox */
|
| 220 |
+
.checkbox-label {
|
| 221 |
+
display: flex;
|
| 222 |
+
align-items: center;
|
| 223 |
+
cursor: pointer;
|
| 224 |
+
font-size: 0.8125rem;
|
| 225 |
+
color: var(--color-text-secondary);
|
| 226 |
+
padding: var(--space-sm);
|
| 227 |
+
border-radius: var(--radius-sm);
|
| 228 |
+
transition: background-color var(--transition-fast);
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.checkbox-label:hover {
|
| 232 |
+
background-color: var(--color-surface-alt);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.checkbox-label input[type="checkbox"] {
|
| 236 |
+
margin-right: var(--space-sm);
|
| 237 |
+
width: 16px;
|
| 238 |
+
height: 16px;
|
| 239 |
+
cursor: pointer;
|
| 240 |
+
accent-color: var(--color-accent);
|
| 241 |
}
|
| 242 |
|
| 243 |
+
/* Charts Container */
|
| 244 |
.charts-container {
|
| 245 |
display: grid;
|
| 246 |
+
grid-template-columns: repeat(2, 1fr);
|
| 247 |
+
gap: var(--space-lg);
|
| 248 |
+
margin-right: 340px; /* Space for floating controls */
|
| 249 |
}
|
| 250 |
|
| 251 |
+
/* Chart Cards */
|
| 252 |
.chart-card {
|
| 253 |
+
background: var(--color-surface);
|
| 254 |
+
border: 1px solid var(--color-border);
|
| 255 |
+
border-radius: var(--radius-md);
|
| 256 |
+
padding: var(--space-lg);
|
| 257 |
+
box-shadow: var(--shadow-sm);
|
| 258 |
+
transition: box-shadow var(--transition-normal);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.chart-card:hover {
|
| 262 |
+
box-shadow: var(--shadow-md);
|
| 263 |
}
|
| 264 |
|
| 265 |
.chart-title {
|
| 266 |
+
font-size: 0.875rem;
|
| 267 |
+
font-weight: 600;
|
| 268 |
+
margin-bottom: var(--space-md);
|
| 269 |
text-align: center;
|
| 270 |
+
color: var(--color-text-primary);
|
| 271 |
+
text-transform: uppercase;
|
| 272 |
+
letter-spacing: 0.03em;
|
| 273 |
}
|
| 274 |
|
| 275 |
.chart-container {
|
| 276 |
position: relative;
|
| 277 |
+
height: 280px;
|
|
|
|
| 278 |
}
|
| 279 |
|
| 280 |
+
/* Confusion Matrix Layout */
|
| 281 |
.confusion-matrix-layout {
|
| 282 |
position: relative;
|
| 283 |
width: 250px;
|
| 284 |
height: 250px;
|
| 285 |
+
margin: 0 auto;
|
| 286 |
+
top: 50%;
|
| 287 |
transform: translateY(-50%);
|
| 288 |
}
|
| 289 |
|
| 290 |
.matrix-ylabel,
|
| 291 |
.matrix-xlabel {
|
| 292 |
position: absolute;
|
| 293 |
+
font-weight: 500;
|
| 294 |
+
color: var(--color-text-secondary);
|
| 295 |
+
font-size: 0.75rem;
|
| 296 |
+
text-transform: uppercase;
|
| 297 |
+
letter-spacing: 0.05em;
|
| 298 |
white-space: nowrap;
|
| 299 |
}
|
| 300 |
|
| 301 |
.matrix-ylabel {
|
| 302 |
top: 50%;
|
| 303 |
+
left: -24px;
|
| 304 |
transform: translateY(-50%) rotate(-90deg);
|
| 305 |
}
|
| 306 |
|
| 307 |
.matrix-xlabel {
|
| 308 |
+
bottom: -28px;
|
| 309 |
left: 50%;
|
| 310 |
transform: translateX(-50%);
|
| 311 |
}
|
|
|
|
| 320 |
max-height: 100%;
|
| 321 |
}
|
| 322 |
|
| 323 |
+
/* Metrics Display (for embedding_distances) */
|
| 324 |
+
.metrics-display {
|
| 325 |
+
margin-top: var(--space-md);
|
| 326 |
+
padding: var(--space-md);
|
| 327 |
+
background: var(--color-surface-alt);
|
| 328 |
+
border: 1px solid var(--color-border);
|
| 329 |
+
border-radius: var(--radius-sm);
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.metrics-title {
|
| 333 |
+
font-weight: 600;
|
| 334 |
+
text-align: center;
|
| 335 |
+
margin-bottom: var(--space-sm);
|
| 336 |
+
color: var(--color-text-primary);
|
| 337 |
+
font-size: 0.75rem;
|
| 338 |
+
text-transform: uppercase;
|
| 339 |
+
letter-spacing: 0.05em;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.metric-item {
|
| 343 |
+
display: flex;
|
| 344 |
+
justify-content: space-between;
|
| 345 |
+
align-items: center;
|
| 346 |
+
padding: var(--space-xs) 0;
|
| 347 |
+
border-bottom: 1px solid var(--color-border);
|
| 348 |
+
cursor: help;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.metric-item:last-child {
|
| 352 |
+
border-bottom: none;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.metric-label {
|
| 356 |
+
font-weight: 400;
|
| 357 |
+
color: var(--color-text-secondary);
|
| 358 |
+
font-size: 0.75rem;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.metric-value {
|
| 362 |
+
font-family: 'SF Mono', Monaco, monospace;
|
| 363 |
+
font-weight: 500;
|
| 364 |
+
color: var(--color-text-primary);
|
| 365 |
+
padding-left: var(--space-sm);
|
| 366 |
+
font-size: 0.8125rem;
|
| 367 |
+
text-align: right;
|
| 368 |
+
min-width: 60px;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.metrics-legend {
|
| 372 |
+
margin-top: var(--space-md);
|
| 373 |
+
padding: var(--space-sm);
|
| 374 |
+
background: var(--color-surface-alt);
|
| 375 |
+
border: 1px solid var(--color-border);
|
| 376 |
+
border-radius: var(--radius-sm);
|
| 377 |
+
text-align: center;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.metrics-legend small {
|
| 381 |
+
font-size: 0.6875rem;
|
| 382 |
+
color: var(--color-text-secondary);
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.metrics-legend span {
|
| 386 |
+
margin: 0 var(--space-xs);
|
| 387 |
+
white-space: nowrap;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
/* Chart Hint */
|
| 391 |
+
.chart-hint {
|
| 392 |
+
margin-top: var(--space-md);
|
| 393 |
+
padding: var(--space-sm) var(--space-md);
|
| 394 |
+
background: var(--color-accent-light);
|
| 395 |
+
border-left: 3px solid var(--color-accent);
|
| 396 |
+
font-size: 0.75rem;
|
| 397 |
+
color: var(--color-text-secondary);
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
/* Footer */
|
| 401 |
footer {
|
| 402 |
text-align: center;
|
| 403 |
+
padding: var(--space-xl);
|
| 404 |
+
margin-top: var(--space-xl);
|
| 405 |
+
border-top: 1px solid var(--color-border);
|
| 406 |
+
margin-right: 340px;
|
| 407 |
}
|
| 408 |
|
| 409 |
footer a {
|
| 410 |
+
color: var(--color-text-secondary);
|
| 411 |
text-decoration: none;
|
| 412 |
+
font-size: 0.8125rem;
|
| 413 |
+
font-weight: 500;
|
| 414 |
+
transition: color var(--transition-fast);
|
| 415 |
}
|
| 416 |
|
| 417 |
footer a:hover {
|
| 418 |
+
color: var(--color-accent);
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
/* Buttons */
|
| 422 |
+
.button-start {
|
| 423 |
+
background-color: var(--color-success);
|
| 424 |
+
border: none;
|
| 425 |
color: white;
|
| 426 |
+
padding: var(--space-md) var(--space-lg);
|
| 427 |
+
text-align: center;
|
| 428 |
+
text-decoration: none;
|
| 429 |
+
display: inline-block;
|
| 430 |
+
font-size: 0.875rem;
|
| 431 |
+
font-weight: 500;
|
| 432 |
+
margin: var(--space-xs) 0;
|
| 433 |
+
cursor: pointer;
|
| 434 |
+
border-radius: var(--radius-sm);
|
| 435 |
+
width: 100%;
|
| 436 |
+
box-sizing: border-box;
|
| 437 |
+
transition: background-color var(--transition-fast);
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.button-start:hover {
|
| 441 |
+
background-color: #1b5e20;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
/* Scrollbar Styling */
|
| 445 |
+
.floating-controls::-webkit-scrollbar {
|
| 446 |
+
width: 6px;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.floating-controls::-webkit-scrollbar-track {
|
| 450 |
+
background: transparent;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
.floating-controls::-webkit-scrollbar-thumb {
|
| 454 |
+
background: var(--color-border);
|
| 455 |
+
border-radius: var(--radius-sm);
|
| 456 |
}
|
| 457 |
|
| 458 |
+
.floating-controls::-webkit-scrollbar-thumb:hover {
|
| 459 |
+
background: var(--color-text-muted);
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
/* Responsive Design */
|
| 463 |
@media (max-width: 1200px) {
|
| 464 |
.charts-container {
|
| 465 |
grid-template-columns: 1fr;
|
| 466 |
+
margin-right: 0;
|
| 467 |
}
|
| 468 |
+
|
| 469 |
+
footer {
|
| 470 |
+
margin-right: 0;
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
.floating-controls {
|
| 474 |
position: static;
|
| 475 |
width: 100%;
|
| 476 |
+
max-width: 600px;
|
| 477 |
+
margin: 0 auto var(--space-lg);
|
| 478 |
+
max-height: none;
|
| 479 |
+
}
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
@media (max-width: 600px) {
|
| 483 |
+
.container {
|
| 484 |
+
padding: var(--space-md);
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
h1 {
|
| 488 |
+
font-size: 1.5rem;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.chart-card {
|
| 492 |
+
padding: var(--space-md);
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.chart-container {
|
| 496 |
+
height: 250px;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.matrix-grid {
|
| 500 |
+
grid-template-columns: 1fr;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
.metric-item {
|
| 504 |
+
flex-direction: column;
|
| 505 |
+
align-items: flex-start;
|
| 506 |
+
gap: var(--space-xs);
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
.metric-value {
|
| 510 |
+
padding-left: 0;
|
| 511 |
+
text-align: left;
|
| 512 |
}
|
| 513 |
}
|
src/js/common.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ============================================================
|
| 2 |
+
// common.js — Shared utilities for School of Statistics
|
| 3 |
+
// ============================================================
|
| 4 |
+
|
| 5 |
+
// --- METRIC EXPLANATIONS (used in tooltip callbacks) ---
|
| 6 |
+
const metricExplanations = {
|
| 7 |
+
'AUC': {
|
| 8 |
+
description: "Probability that a randomly chosen positive instance is ranked higher than a randomly chosen negative instance.",
|
| 9 |
+
range: "0 to 1. 0.5 corresponds to random chance.",
|
| 10 |
+
formula: "Area Under the ROC Curve"
|
| 11 |
+
},
|
| 12 |
+
'Accuracy': {
|
| 13 |
+
description: "Proportion of all predictions that are correct.",
|
| 14 |
+
range: "0 to 1.",
|
| 15 |
+
formula: "(TP + TN) / (TP + TN + FP + FN)"
|
| 16 |
+
},
|
| 17 |
+
'Precision': {
|
| 18 |
+
description: "Proportion of positive predictions that are correct. A high value indicates a low false positive rate.",
|
| 19 |
+
range: "0 to 1.",
|
| 20 |
+
formula: "TP / (TP + FP)"
|
| 21 |
+
},
|
| 22 |
+
'Recall': {
|
| 23 |
+
description: "Proportion of actual positives correctly identified. Also called Sensitivity or True Positive Rate.",
|
| 24 |
+
range: "0 to 1.",
|
| 25 |
+
formula: "TP / (TP + FN)"
|
| 26 |
+
},
|
| 27 |
+
'Specificity': {
|
| 28 |
+
description: "Proportion of actual negatives correctly identified. Also called True Negative Rate.",
|
| 29 |
+
range: "0 to 1.",
|
| 30 |
+
formula: "TN / (TN + FP)"
|
| 31 |
+
},
|
| 32 |
+
'F1-Score': {
|
| 33 |
+
description: "Harmonic mean of Precision and Recall. Balances both metrics in a single value.",
|
| 34 |
+
range: "0 to 1.",
|
| 35 |
+
formula: "2 * (Precision * Recall) / (Precision + Recall)"
|
| 36 |
+
}
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
// --- RANDOM GAUSSIAN (Box-Muller transform) ---
|
| 40 |
+
function randomGaussian(mean = 0, stdDev = 1) {
|
| 41 |
+
let u = 0, v = 0;
|
| 42 |
+
while (u === 0) u = Math.random();
|
| 43 |
+
while (v === 0) v = Math.random();
|
| 44 |
+
return mean + stdDev * Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// --- ROC CURVE & AUC CALCULATION ---
|
| 48 |
+
function calculateRocAndAuc(labels, scores) {
|
| 49 |
+
const pairs = labels.map((label, i) => ({ label, score: scores[i] }));
|
| 50 |
+
pairs.sort((a, b) => b.score - a.score);
|
| 51 |
+
let tp = 0, fp = 0;
|
| 52 |
+
const total_pos = labels.filter(l => l === 1).length;
|
| 53 |
+
const total_neg = labels.length - total_pos;
|
| 54 |
+
if (total_pos === 0 || total_neg === 0) {
|
| 55 |
+
return { rocPoints: [{ x: 0, y: 0 }, { x: 1, y: 1 }], auc: 0.5 };
|
| 56 |
+
}
|
| 57 |
+
const rocPoints = [{ x: 0, y: 0 }];
|
| 58 |
+
let auc = 0, prev_tpr = 0, prev_fpr = 0;
|
| 59 |
+
for (const pair of pairs) {
|
| 60 |
+
if (pair.label === 1) tp++; else fp++;
|
| 61 |
+
const tpr = total_pos > 0 ? tp / total_pos : 0;
|
| 62 |
+
const fpr = total_neg > 0 ? fp / total_neg : 0;
|
| 63 |
+
auc += (tpr + prev_tpr) / 2 * (fpr - prev_fpr);
|
| 64 |
+
rocPoints.push({ x: fpr, y: tpr });
|
| 65 |
+
prev_tpr = tpr;
|
| 66 |
+
prev_fpr = fpr;
|
| 67 |
+
}
|
| 68 |
+
return { rocPoints, auc };
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// --- CONFUSION MATRIX DRAWING (Canvas 2D) ---
|
| 72 |
+
function drawConfusionMatrix(canvasId, tp, fp, tn, fn) {
|
| 73 |
+
const canvas = document.getElementById(canvasId);
|
| 74 |
+
const ctx = canvas.getContext('2d');
|
| 75 |
+
const w = canvas.width, h = canvas.height;
|
| 76 |
+
ctx.clearRect(0, 0, w, h);
|
| 77 |
+
|
| 78 |
+
const margin = 50;
|
| 79 |
+
const gridW = w - margin, gridH = h - margin;
|
| 80 |
+
const cellW = gridW / 2, cellH = gridH / 2;
|
| 81 |
+
const max_val = Math.max(tp, fp, tn, fn);
|
| 82 |
+
const baseColor = [8, 48, 107];
|
| 83 |
+
|
| 84 |
+
const cells = [
|
| 85 |
+
{ label: 'TN', value: tn, x: 0, y: cellH },
|
| 86 |
+
{ label: 'FP', value: fp, x: cellW, y: cellH },
|
| 87 |
+
{ label: 'FN', value: fn, x: 0, y: 0 },
|
| 88 |
+
{ label: 'TP', value: tp, x: cellW, y: 0 }
|
| 89 |
+
];
|
| 90 |
+
|
| 91 |
+
cells.forEach(cell => {
|
| 92 |
+
const intensity = max_val > 0 ? cell.value / max_val : 0;
|
| 93 |
+
ctx.fillStyle = `rgba(${baseColor[0]}, ${baseColor[1]}, ${baseColor[2]}, ${intensity})`;
|
| 94 |
+
ctx.fillRect(margin + cell.x, cell.y, cellW, cellH);
|
| 95 |
+
ctx.fillStyle = intensity > 0.5 ? 'white' : 'black';
|
| 96 |
+
ctx.textAlign = 'center';
|
| 97 |
+
ctx.textBaseline = 'middle';
|
| 98 |
+
ctx.font = 'bold 20px Segoe UI';
|
| 99 |
+
ctx.fillText(cell.label, margin + cell.x + cellW / 2, cell.y + cellH / 2 - 12);
|
| 100 |
+
ctx.font = '18px Segoe UI';
|
| 101 |
+
ctx.fillText(cell.value, margin + cell.x + cellW / 2, cell.y + cellH / 2 + 12);
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
ctx.fillStyle = '#333';
|
| 105 |
+
ctx.font = 'bold 14px Segoe UI';
|
| 106 |
+
ctx.fillText('Negative', margin + cellW / 2, gridH + 20);
|
| 107 |
+
ctx.fillText('Positive', margin + cellW + cellW / 2, gridH + 20);
|
| 108 |
+
ctx.save();
|
| 109 |
+
ctx.translate(20, gridH / 2);
|
| 110 |
+
ctx.rotate(-Math.PI / 2);
|
| 111 |
+
ctx.textAlign = 'center';
|
| 112 |
+
ctx.textBaseline = 'middle';
|
| 113 |
+
ctx.fillText('Positive', -cellH / 2, 0);
|
| 114 |
+
ctx.fillText('Negative', cellH / 2, 0);
|
| 115 |
+
ctx.restore();
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// --- CUSTOM DATALABELS PLUGIN (for bar charts) ---
|
| 119 |
+
const customDatalabelsPlugin = {
|
| 120 |
+
id: 'customDatalabels',
|
| 121 |
+
afterDatasetsDraw: (chart) => {
|
| 122 |
+
const ctx = chart.ctx;
|
| 123 |
+
ctx.save();
|
| 124 |
+
ctx.font = 'bold 12px Segoe UI';
|
| 125 |
+
ctx.fillStyle = 'white';
|
| 126 |
+
ctx.textAlign = 'center';
|
| 127 |
+
chart.data.datasets.forEach((dataset, i) => {
|
| 128 |
+
const meta = chart.getDatasetMeta(i);
|
| 129 |
+
meta.data.forEach((bar, index) => {
|
| 130 |
+
const data = dataset.data[index];
|
| 131 |
+
if (bar.height > 15) {
|
| 132 |
+
ctx.textBaseline = 'bottom';
|
| 133 |
+
ctx.fillText(data.toFixed(3), bar.x, bar.y + bar.height - 5);
|
| 134 |
+
}
|
| 135 |
+
});
|
| 136 |
+
});
|
| 137 |
+
ctx.restore();
|
| 138 |
+
}
|
| 139 |
+
};
|
| 140 |
+
|
| 141 |
+
// --- DEBOUNCE UTILITY ---
|
| 142 |
+
function debounce(func, wait) {
|
| 143 |
+
let timeout;
|
| 144 |
+
return function executedFunction(...args) {
|
| 145 |
+
const later = () => {
|
| 146 |
+
clearTimeout(timeout);
|
| 147 |
+
func(...args);
|
| 148 |
+
};
|
| 149 |
+
clearTimeout(timeout);
|
| 150 |
+
timeout = setTimeout(later, wait);
|
| 151 |
+
};
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
// --- DRAGGABLE CONTROLS PANEL ---
|
| 155 |
+
function makeDraggable(element, handle) {
|
| 156 |
+
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
| 157 |
+
let isDragging = false;
|
| 158 |
+
|
| 159 |
+
handle.onmousedown = (e) => {
|
| 160 |
+
// Don't drag if clicking on a form control (slider, input, etc.)
|
| 161 |
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' ||
|
| 162 |
+
e.target.tagName === 'TEXTAREA' || e.target.classList.contains('slider')) {
|
| 163 |
+
return;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
isDragging = true;
|
| 167 |
+
pos3 = e.clientX;
|
| 168 |
+
pos4 = e.clientY;
|
| 169 |
+
document.onmouseup = closeDragElement;
|
| 170 |
+
document.onmousemove = elementDrag;
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
const elementDrag = (e) => {
|
| 174 |
+
if (!isDragging) return;
|
| 175 |
+
e.preventDefault();
|
| 176 |
+
pos1 = pos3 - e.clientX;
|
| 177 |
+
pos2 = pos4 - e.clientY;
|
| 178 |
+
pos3 = e.clientX;
|
| 179 |
+
pos4 = e.clientY;
|
| 180 |
+
element.style.top = (element.offsetTop - pos2) + "px";
|
| 181 |
+
element.style.left = (element.offsetLeft - pos1) + "px";
|
| 182 |
+
};
|
| 183 |
+
|
| 184 |
+
const closeDragElement = () => {
|
| 185 |
+
isDragging = false;
|
| 186 |
+
document.onmouseup = null;
|
| 187 |
+
document.onmousemove = null;
|
| 188 |
+
};
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// --- METRICS BAR CHART TOOLTIP CALLBACK ---
|
| 192 |
+
function metricsTooltipCallback(context) {
|
| 193 |
+
const label = context.chart.data.labels[context.dataIndex];
|
| 194 |
+
const value = context.raw.toFixed(3);
|
| 195 |
+
const explanation = metricExplanations[label];
|
| 196 |
+
let tooltipText = [`${label}: ${value}`];
|
| 197 |
+
if (explanation) {
|
| 198 |
+
tooltipText.push('');
|
| 199 |
+
const roleLines = `${explanation.description}`.match(/.{1,50}(\s|$)/g) || [];
|
| 200 |
+
roleLines.forEach(line => tooltipText.push(line.trim()));
|
| 201 |
+
tooltipText.push(`Range: ${explanation.range}`);
|
| 202 |
+
tooltipText.push(`Formula: ${explanation.formula}`);
|
| 203 |
+
}
|
| 204 |
+
return tooltipText;
|
| 205 |
+
}
|
src/js/direct_classifier.js
CHANGED
|
@@ -2,47 +2,6 @@
|
|
| 2 |
let dataChart, rocChart, metricsChart;
|
| 3 |
const N_SAMPLES_PER_CLASS = 100;
|
| 4 |
|
| 5 |
-
const metricExplanations = {
|
| 6 |
-
'AUC': {
|
| 7 |
-
description: "Measures the model's ability to distinguish between positive and negative classes. It represents the probability that a random positive instance is ranked higher than a random negative instance.",
|
| 8 |
-
range: "Ranges from 0 (worst) to 1 (best). 0.5 is random chance.",
|
| 9 |
-
formula: "Area Under the ROC Curve"
|
| 10 |
-
},
|
| 11 |
-
'Accuracy': {
|
| 12 |
-
description: "The proportion of all predictions that are correct. It's a general measure of the model's performance.",
|
| 13 |
-
range: "Ranges from 0 (worst) to 1 (best).",
|
| 14 |
-
formula: "(TP + TN) / (TP + TN + FP + FN)"
|
| 15 |
-
},
|
| 16 |
-
'Precision': {
|
| 17 |
-
description: "Of all the positive predictions made by the model, how many were actually positive. High precision indicates a low false positive rate.",
|
| 18 |
-
range: "Ranges from 0 (worst) to 1 (best).",
|
| 19 |
-
formula: "TP / (TP + FP)"
|
| 20 |
-
},
|
| 21 |
-
'Recall': {
|
| 22 |
-
description: "Of all the actual positive instances, how many did the model correctly identify. Also known as Sensitivity or True Positive Rate.",
|
| 23 |
-
range: "Ranges from 0 (worst) to 1 (best).",
|
| 24 |
-
formula: "TP / (TP + FN)"
|
| 25 |
-
},
|
| 26 |
-
'Specificity': {
|
| 27 |
-
description: "Of all the actual negative instances, how many did the model correctly identify. Also known as True Negative Rate.",
|
| 28 |
-
range: "Ranges from 0 (worst) to 1 (best).",
|
| 29 |
-
formula: "TN / (TN + FP)"
|
| 30 |
-
},
|
| 31 |
-
'F1-Score': {
|
| 32 |
-
description: "The harmonic mean of Precision and Recall. It provides a single score that balances both concerns, useful for imbalanced classes.",
|
| 33 |
-
range: "Ranges from 0 (worst) to 1 (best).",
|
| 34 |
-
formula: "2 * (Precision * Recall) / (Precision + Recall)"
|
| 35 |
-
}
|
| 36 |
-
};
|
| 37 |
-
|
| 38 |
-
// --- DATA GENERATION ---
|
| 39 |
-
function randomGaussian(mean = 0, stdDev = 1) {
|
| 40 |
-
let u = 0, v = 0;
|
| 41 |
-
while (u === 0) u = Math.random();
|
| 42 |
-
while (v === 0) v = Math.random();
|
| 43 |
-
return mean + stdDev * Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
function generateData(separation, stdDev) {
|
| 47 |
const data = [], labels = [];
|
| 48 |
for (let i = 0; i < N_SAMPLES_PER_CLASS; i++) { data.push({ x: randomGaussian(-separation / 2, stdDev), y: randomGaussian(0, stdDev) }); labels.push(0); }
|
|
@@ -86,67 +45,16 @@ class GaussianNB {
|
|
| 86 |
}
|
| 87 |
|
| 88 |
// --- METRICS CALCULATIONS ---
|
| 89 |
-
function calculateRocAndAuc(labels, scores) {
|
| 90 |
-
const pairs = labels.map((label, i) => ({ label, score: scores[i] }));
|
| 91 |
-
pairs.sort((a, b) => b.score - a.score);
|
| 92 |
-
let tp = 0, fp = 0;
|
| 93 |
-
const total_pos = labels.filter(l => l === 1).length;
|
| 94 |
-
const total_neg = labels.length - total_pos;
|
| 95 |
-
if (total_pos === 0 || total_neg === 0) return { rocPoints: [{ x: 0, y: 0 }, { x: 1, y: 1 }], auc: 0.5 };
|
| 96 |
-
const rocPoints = [{ x: 0, y: 0 }];
|
| 97 |
-
let auc = 0, prev_tpr = 0, prev_fpr = 0;
|
| 98 |
-
for (const pair of pairs) {
|
| 99 |
-
if (pair.label === 1) tp++; else fp++;
|
| 100 |
-
const tpr = tp / total_pos;
|
| 101 |
-
const fpr = fp / total_neg;
|
| 102 |
-
auc += (tpr + prev_tpr) / 2 * (fpr - prev_fpr);
|
| 103 |
-
rocPoints.push({ x: fpr, y: tpr });
|
| 104 |
-
prev_tpr = tpr; prev_fpr = fpr;
|
| 105 |
-
}
|
| 106 |
-
return { rocPoints, auc };
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
function getConfusionMatrix(labels, scores, threshold) {
|
| 110 |
-
let
|
| 111 |
labels.forEach((label, i) => {
|
| 112 |
const prediction = scores[i] >= threshold ? 1 : 0;
|
| 113 |
-
if (prediction === 1 && label === 1)
|
| 114 |
else if (prediction === 1 && label === 0) fp++;
|
| 115 |
-
else if (prediction === 0 && label === 0)
|
| 116 |
else if (prediction === 0 && label === 1) fn++;
|
| 117 |
});
|
| 118 |
-
return {
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
function drawConfusionMatrix(canvasId, vp, fp, vn, fn) {
|
| 122 |
-
const canvas = document.getElementById(canvasId);
|
| 123 |
-
const ctx = canvas.getContext('2d');
|
| 124 |
-
const w = canvas.width, h = canvas.height;
|
| 125 |
-
ctx.clearRect(0, 0, w, h);
|
| 126 |
-
const margin = 50, gridW = w - margin, gridH = h - margin, cellW = gridW / 2, cellH = gridH / 2;
|
| 127 |
-
const max_val = Math.max(vp, fp, vn, fn);
|
| 128 |
-
const baseColor = [8, 48, 107];
|
| 129 |
-
const cells = [{ label: 'TN', value: vn, x: 0, y: cellH }, { label: 'FP', value: fp, x: cellW, y: cellH }, { label: 'FN', value: fn, x: 0, y: 0 }, { label: 'TP', value: vp, x: cellW, y: 0 }];
|
| 130 |
-
cells.forEach(cell => {
|
| 131 |
-
const intensity = max_val > 0 ? cell.value / max_val : 0;
|
| 132 |
-
ctx.fillStyle = `rgba(${baseColor[0]}, ${baseColor[1]}, ${baseColor[2]}, ${intensity})`;
|
| 133 |
-
ctx.fillRect(margin + cell.x, cell.y, cellW, cellH);
|
| 134 |
-
ctx.fillStyle = intensity > 0.5 ? 'white' : 'black';
|
| 135 |
-
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
| 136 |
-
ctx.font = 'bold 20px Segoe UI'; ctx.fillText(cell.label, margin + cell.x + cellW / 2, cell.y + cellH / 2 - 12);
|
| 137 |
-
ctx.font = '18px Segoe UI'; ctx.fillText(cell.value, margin + cell.x + cellW / 2, cell.y + cellH / 2 + 12);
|
| 138 |
-
});
|
| 139 |
-
ctx.fillStyle = '#333'; ctx.font = 'bold 14px Segoe UI';
|
| 140 |
-
ctx.fillText('Negative', margin + cellW / 2, gridH + 20);
|
| 141 |
-
ctx.fillText('Positive', margin + cellW + cellW / 2, gridH + 20);
|
| 142 |
-
ctx.save();
|
| 143 |
-
ctx.translate(20, gridH / 2);
|
| 144 |
-
ctx.rotate(-Math.PI / 2);
|
| 145 |
-
ctx.textAlign = 'center';
|
| 146 |
-
ctx.textBaseline = 'middle';
|
| 147 |
-
ctx.fillText('Positive', -cellH / 2, 0);
|
| 148 |
-
ctx.fillText('Negative', cellH / 2, 0);
|
| 149 |
-
ctx.restore();
|
| 150 |
}
|
| 151 |
|
| 152 |
// --- UI UPDATE ---
|
|
@@ -161,15 +69,15 @@ function updateApplication() {
|
|
| 161 |
model.fit(data, labels);
|
| 162 |
const scores = model.predict_proba(data);
|
| 163 |
const { rocPoints, auc } = calculateRocAndAuc(labels, scores);
|
| 164 |
-
const {
|
| 165 |
-
const total =
|
| 166 |
-
const precision = (
|
| 167 |
-
const recall = (
|
| 168 |
-
const specificity = (
|
| 169 |
const f1score = (precision + recall) > 0 ? 2 * (precision * recall) / (precision + recall) : 0;
|
| 170 |
-
const accuracy = total > 0 ? (
|
| 171 |
|
| 172 |
-
drawConfusionMatrix('matrixChart',
|
| 173 |
|
| 174 |
dataChart.data.datasets[0].data = data.filter((_, i) => labels[i] === 0);
|
| 175 |
dataChart.data.datasets[1].data = data.filter((_, i) => labels[i] === 1);
|
|
@@ -183,28 +91,6 @@ function updateApplication() {
|
|
| 183 |
}
|
| 184 |
|
| 185 |
// --- INITIALIZATION ---
|
| 186 |
-
const customDatalabelsPlugin = {
|
| 187 |
-
id: 'customDatalabels',
|
| 188 |
-
afterDatasetsDraw: (chart) => {
|
| 189 |
-
const ctx = chart.ctx;
|
| 190 |
-
ctx.save();
|
| 191 |
-
ctx.font = 'bold 12px Segoe UI';
|
| 192 |
-
ctx.fillStyle = 'white';
|
| 193 |
-
ctx.textAlign = 'center';
|
| 194 |
-
chart.data.datasets.forEach((dataset, i) => {
|
| 195 |
-
const meta = chart.getDatasetMeta(i);
|
| 196 |
-
meta.data.forEach((bar, index) => {
|
| 197 |
-
const data = dataset.data[index];
|
| 198 |
-
if (bar.height > 15) {
|
| 199 |
-
ctx.textBaseline = 'bottom';
|
| 200 |
-
ctx.fillText(data.toFixed(3), bar.x, bar.y + bar.height - 5);
|
| 201 |
-
}
|
| 202 |
-
});
|
| 203 |
-
});
|
| 204 |
-
ctx.restore();
|
| 205 |
-
}
|
| 206 |
-
};
|
| 207 |
-
|
| 208 |
function initCharts() {
|
| 209 |
const dataCtx = document.getElementById('dataChart').getContext('2d');
|
| 210 |
dataChart = new Chart(dataCtx, { type: 'scatter', data: { datasets: [{ label: 'Negative Class', data: [], backgroundColor: '#0D47A1' }, { label: 'Positive Class', data: [], backgroundColor: '#B71C1C' }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0 } } });
|
|
@@ -232,20 +118,7 @@ function initCharts() {
|
|
| 232 |
padding: 15,
|
| 233 |
displayColors: false,
|
| 234 |
callbacks: {
|
| 235 |
-
label:
|
| 236 |
-
const label = context.chart.data.labels[context.dataIndex];
|
| 237 |
-
const value = context.raw.toFixed(3);
|
| 238 |
-
const explanation = metricExplanations[label];
|
| 239 |
-
let tooltipText = [`${label}: ${value}`];
|
| 240 |
-
if (explanation) {
|
| 241 |
-
tooltipText.push('');
|
| 242 |
-
const roleLines = `Role: ${explanation.description}`.match(/.{1,50}(\s|$)/g) || [];
|
| 243 |
-
roleLines.forEach(line => tooltipText.push(line.trim()));
|
| 244 |
-
tooltipText.push(`Range: ${explanation.range}`);
|
| 245 |
-
tooltipText.push(`Formula: ${explanation.formula}`);
|
| 246 |
-
}
|
| 247 |
-
return tooltipText;
|
| 248 |
-
}
|
| 249 |
}
|
| 250 |
}
|
| 251 |
},
|
|
@@ -254,13 +127,6 @@ function initCharts() {
|
|
| 254 |
});
|
| 255 |
}
|
| 256 |
|
| 257 |
-
function makeDraggable(element, handle) {
|
| 258 |
-
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
| 259 |
-
handle.onmousedown = (e) => { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; };
|
| 260 |
-
const elementDrag = (e) => { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; element.style.top = (element.offsetTop - pos2) + "px"; element.style.left = (element.offsetLeft - pos1) + "px"; };
|
| 261 |
-
const closeDragElement = () => { document.onmouseup = null; document.onmousemove = null; };
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
window.addEventListener('load', function () {
|
| 265 |
initCharts();
|
| 266 |
const sliders = ['separationSlider', 'stdDevSlider'];
|
|
|
|
| 2 |
let dataChart, rocChart, metricsChart;
|
| 3 |
const N_SAMPLES_PER_CLASS = 100;
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
function generateData(separation, stdDev) {
|
| 6 |
const data = [], labels = [];
|
| 7 |
for (let i = 0; i < N_SAMPLES_PER_CLASS; i++) { data.push({ x: randomGaussian(-separation / 2, stdDev), y: randomGaussian(0, stdDev) }); labels.push(0); }
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
// --- METRICS CALCULATIONS ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
function getConfusionMatrix(labels, scores, threshold) {
|
| 49 |
+
let tp = 0, fp = 0, tn = 0, fn = 0;
|
| 50 |
labels.forEach((label, i) => {
|
| 51 |
const prediction = scores[i] >= threshold ? 1 : 0;
|
| 52 |
+
if (prediction === 1 && label === 1) tp++;
|
| 53 |
else if (prediction === 1 && label === 0) fp++;
|
| 54 |
+
else if (prediction === 0 && label === 0) tn++;
|
| 55 |
else if (prediction === 0 && label === 1) fn++;
|
| 56 |
});
|
| 57 |
+
return { tp, fp, tn, fn };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
// --- UI UPDATE ---
|
|
|
|
| 69 |
model.fit(data, labels);
|
| 70 |
const scores = model.predict_proba(data);
|
| 71 |
const { rocPoints, auc } = calculateRocAndAuc(labels, scores);
|
| 72 |
+
const { tp, fp, tn, fn } = getConfusionMatrix(labels, scores, 0.5);
|
| 73 |
+
const total = tp + fp + tn + fn;
|
| 74 |
+
const precision = (tp + fp) > 0 ? tp / (tp + fp) : 0;
|
| 75 |
+
const recall = (tp + fn) > 0 ? tp / (tp + fn) : 0;
|
| 76 |
+
const specificity = (tn + fp) > 0 ? tn / (tn + fp) : 0;
|
| 77 |
const f1score = (precision + recall) > 0 ? 2 * (precision * recall) / (precision + recall) : 0;
|
| 78 |
+
const accuracy = total > 0 ? (tp + tn) / total : 0;
|
| 79 |
|
| 80 |
+
drawConfusionMatrix('matrixChart', tp, fp, tn, fn);
|
| 81 |
|
| 82 |
dataChart.data.datasets[0].data = data.filter((_, i) => labels[i] === 0);
|
| 83 |
dataChart.data.datasets[1].data = data.filter((_, i) => labels[i] === 1);
|
|
|
|
| 91 |
}
|
| 92 |
|
| 93 |
// --- INITIALIZATION ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
function initCharts() {
|
| 95 |
const dataCtx = document.getElementById('dataChart').getContext('2d');
|
| 96 |
dataChart = new Chart(dataCtx, { type: 'scatter', data: { datasets: [{ label: 'Negative Class', data: [], backgroundColor: '#0D47A1' }, { label: 'Positive Class', data: [], backgroundColor: '#B71C1C' }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0 } } });
|
|
|
|
| 118 |
padding: 15,
|
| 119 |
displayColors: false,
|
| 120 |
callbacks: {
|
| 121 |
+
label: metricsTooltipCallback
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
}
|
| 123 |
}
|
| 124 |
},
|
|
|
|
| 127 |
});
|
| 128 |
}
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
window.addEventListener('load', function () {
|
| 131 |
initCharts();
|
| 132 |
const sliders = ['separationSlider', 'stdDevSlider'];
|
src/js/fourier_transform.js
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ============================================================
|
| 2 |
+
// fourier_transform.js — Fourier Transform Visualizer
|
| 3 |
+
// ============================================================
|
| 4 |
+
|
| 5 |
+
// --- GLOBAL VARIABLES ---
|
| 6 |
+
let timeDomainChart, magnitudeChart, phaseChart;
|
| 7 |
+
|
| 8 |
+
// Wave colors matching the HTML
|
| 9 |
+
const WAVE_COLORS = ['#1976d2', '#d32f2f', '#388e3c', '#f57c00'];
|
| 10 |
+
|
| 11 |
+
// Sampling parameters
|
| 12 |
+
const SAMPLING_RATE = 256; // Hz - Fixed sampling rate for display
|
| 13 |
+
const MAX_SAMPLES_FOR_DFT = 512; // Maximum samples for DFT to maintain performance
|
| 14 |
+
|
| 15 |
+
// --- DFT IMPLEMENTATION ---
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Cooley-Tukey FFT algorithm for power-of-2 sizes
|
| 19 |
+
* Much faster than DFT for large N
|
| 20 |
+
*
|
| 21 |
+
* @param {number[]} signal - Real-valued input signal
|
| 22 |
+
* @returns {Object} - Object with real (re) and imaginary (im) parts arrays
|
| 23 |
+
*/
|
| 24 |
+
function fft(signal) {
|
| 25 |
+
const N = signal.length;
|
| 26 |
+
|
| 27 |
+
// Check if N is a power of 2
|
| 28 |
+
if (N & (N - 1)) {
|
| 29 |
+
// Not a power of 2, fall back to DFT
|
| 30 |
+
return dft(signal);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Bit-reverse copy
|
| 34 |
+
const re = new Array(N);
|
| 35 |
+
const im = new Array(N).fill(0);
|
| 36 |
+
|
| 37 |
+
for (let i = 0; i < N; i++) {
|
| 38 |
+
let j = 0;
|
| 39 |
+
let bit = N >> 1;
|
| 40 |
+
let ii = i;
|
| 41 |
+
while (bit > 0) {
|
| 42 |
+
if (ii & 1) j |= bit;
|
| 43 |
+
ii >>= 1;
|
| 44 |
+
bit >>= 1;
|
| 45 |
+
}
|
| 46 |
+
re[j] = signal[i];
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Butterfly operations
|
| 50 |
+
for (let step = 2; step <= N; step <<= 1) {
|
| 51 |
+
const halfStep = step >> 1;
|
| 52 |
+
const angleInc = -2 * Math.PI / step;
|
| 53 |
+
|
| 54 |
+
for (let group = 0; group < N; group += step) {
|
| 55 |
+
for (let i = 0; i < halfStep; i++) {
|
| 56 |
+
const angle = angleInc * i;
|
| 57 |
+
const cos = Math.cos(angle);
|
| 58 |
+
const sin = Math.sin(angle);
|
| 59 |
+
|
| 60 |
+
const evenRe = re[group + i];
|
| 61 |
+
const evenIm = im[group + i];
|
| 62 |
+
const oddRe = re[group + i + halfStep] * cos - im[group + i + halfStep] * sin;
|
| 63 |
+
const oddIm = re[group + i + halfStep] * sin + im[group + i + halfStep] * cos;
|
| 64 |
+
|
| 65 |
+
re[group + i] = evenRe + oddRe;
|
| 66 |
+
im[group + i] = evenIm + oddIm;
|
| 67 |
+
re[group + i + halfStep] = evenRe - oddRe;
|
| 68 |
+
im[group + i + halfStep] = evenIm - oddIm;
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
return { re, im };
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/**
|
| 77 |
+
* Discrete Fourier Transform (DFT) - O(N²) implementation
|
| 78 |
+
* Input: real-valued time domain signal
|
| 79 |
+
* Output: complex frequency domain values
|
| 80 |
+
*
|
| 81 |
+
* @param {number[]} signal - Real-valued input signal
|
| 82 |
+
* @returns {Object} - Object with real (re) and imaginary (im) parts arrays
|
| 83 |
+
*/
|
| 84 |
+
function dft(signal) {
|
| 85 |
+
const N = signal.length;
|
| 86 |
+
const re = new Array(N).fill(0);
|
| 87 |
+
const im = new Array(N).fill(0);
|
| 88 |
+
|
| 89 |
+
for (let k = 0; k < N; k++) {
|
| 90 |
+
let sumRe = 0;
|
| 91 |
+
let sumIm = 0;
|
| 92 |
+
for (let n = 0; n < N; n++) {
|
| 93 |
+
const angle = -2 * Math.PI * k * n / N;
|
| 94 |
+
sumRe += signal[n] * Math.cos(angle);
|
| 95 |
+
sumIm += signal[n] * Math.sin(angle);
|
| 96 |
+
}
|
| 97 |
+
re[k] = sumRe;
|
| 98 |
+
im[k] = sumIm;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
return { re, im };
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/**
|
| 105 |
+
* Compute magnitude spectrum from complex DFT output
|
| 106 |
+
* Returns single-sided spectrum (positive frequencies only)
|
| 107 |
+
*
|
| 108 |
+
* @param {Object} dftResult - Object with re and im arrays
|
| 109 |
+
* @returns {number[]} - Magnitude values for positive frequencies
|
| 110 |
+
*/
|
| 111 |
+
function computeMagnitudeSpectrum(dftResult) {
|
| 112 |
+
const N = dftResult.re.length;
|
| 113 |
+
const magnitudes = [];
|
| 114 |
+
|
| 115 |
+
// Only return positive frequencies (0 to N/2)
|
| 116 |
+
for (let k = 0; k <= N / 2; k++) {
|
| 117 |
+
const mag = Math.sqrt(dftResult.re[k] ** 2 + dftResult.im[k] ** 2);
|
| 118 |
+
// Normalize by N/2 for single-sided spectrum
|
| 119 |
+
magnitudes.push(2 * mag / N);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// DC component should not be doubled
|
| 123 |
+
magnitudes[0] = magnitudes[0] / 2;
|
| 124 |
+
if (N % 2 === 0) {
|
| 125 |
+
magnitudes[magnitudes.length - 1] = magnitudes[magnitudes.length - 1] / 2;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
return magnitudes;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/**
|
| 132 |
+
* Compute phase spectrum from complex DFT output
|
| 133 |
+
* Returns single-sided spectrum (positive frequencies only)
|
| 134 |
+
*
|
| 135 |
+
* @param {Object} dftResult - Object with re and im arrays
|
| 136 |
+
* @returns {number[]} - Phase values in degrees for positive frequencies
|
| 137 |
+
*/
|
| 138 |
+
function computePhaseSpectrum(dftResult) {
|
| 139 |
+
const N = dftResult.re.length;
|
| 140 |
+
const phases = [];
|
| 141 |
+
|
| 142 |
+
for (let k = 0; k <= N / 2; k++) {
|
| 143 |
+
const phaseRad = Math.atan2(dftResult.im[k], dftResult.re[k]);
|
| 144 |
+
const phaseDeg = phaseRad * 180 / Math.PI;
|
| 145 |
+
phases.push(phaseDeg);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
return phases;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// --- SIGNAL GENERATION ---
|
| 152 |
+
|
| 153 |
+
/**
|
| 154 |
+
* Generate a sine wave
|
| 155 |
+
*
|
| 156 |
+
* @param {number} frequency - Frequency in Hz
|
| 157 |
+
* @param {number} amplitude - Peak amplitude
|
| 158 |
+
* @param {number} phaseDeg - Phase in degrees
|
| 159 |
+
* @param {number} samplingRate - Sampling rate in Hz
|
| 160 |
+
* @param {number} numSamples - Number of samples
|
| 161 |
+
* @returns {number[]} - Generated signal
|
| 162 |
+
*/
|
| 163 |
+
function generateSineWave(frequency, amplitude, phaseDeg, samplingRate, numSamples) {
|
| 164 |
+
const signal = [];
|
| 165 |
+
const phaseRad = phaseDeg * Math.PI / 180;
|
| 166 |
+
|
| 167 |
+
for (let n = 0; n < numSamples; n++) {
|
| 168 |
+
const t = n / samplingRate;
|
| 169 |
+
const value = amplitude * Math.sin(2 * Math.PI * frequency * t + phaseRad);
|
| 170 |
+
signal.push(value);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
return signal;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/**
|
| 177 |
+
* Generate the composite signal from all enabled waves
|
| 178 |
+
*
|
| 179 |
+
* @returns {Object} - Object with time array, signal array, and individual waves
|
| 180 |
+
*/
|
| 181 |
+
function generateCompositeSignal() {
|
| 182 |
+
const numSamples = parseInt(document.getElementById('numSamples').value);
|
| 183 |
+
const addNoise = document.getElementById('addNoise').checked;
|
| 184 |
+
const noiseLevel = parseFloat(document.getElementById('noiseLevel').value);
|
| 185 |
+
|
| 186 |
+
// Initialize signal and time arrays
|
| 187 |
+
const time = [];
|
| 188 |
+
const signal = new Array(numSamples).fill(0);
|
| 189 |
+
const individualWaves = [];
|
| 190 |
+
|
| 191 |
+
// Generate time array
|
| 192 |
+
for (let n = 0; n < numSamples; n++) {
|
| 193 |
+
time.push(n / SAMPLING_RATE);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
// Add each enabled wave
|
| 197 |
+
for (let i = 1; i <= 4; i++) {
|
| 198 |
+
const enabled = document.getElementById(`enableWave${i}`).checked;
|
| 199 |
+
if (!enabled) continue;
|
| 200 |
+
|
| 201 |
+
const frequency = parseInt(document.getElementById(`freq${i}`).value);
|
| 202 |
+
const amplitude = parseFloat(document.getElementById(`amp${i}`).value);
|
| 203 |
+
const phase = parseInt(document.getElementById(`phase${i}`).value);
|
| 204 |
+
|
| 205 |
+
if (amplitude > 0) {
|
| 206 |
+
const wave = generateSineWave(frequency, amplitude, phase, SAMPLING_RATE, numSamples);
|
| 207 |
+
individualWaves.push({
|
| 208 |
+
frequency,
|
| 209 |
+
amplitude,
|
| 210 |
+
phase,
|
| 211 |
+
color: WAVE_COLORS[i - 1],
|
| 212 |
+
data: wave
|
| 213 |
+
});
|
| 214 |
+
|
| 215 |
+
// Add to composite signal
|
| 216 |
+
for (let n = 0; n < numSamples; n++) {
|
| 217 |
+
signal[n] += wave[n];
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
// Add noise if enabled
|
| 223 |
+
if (addNoise) {
|
| 224 |
+
for (let n = 0; n < numSamples; n++) {
|
| 225 |
+
signal[n] += randomGaussian(0, noiseLevel);
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
return { time, signal, individualWaves, numSamples };
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
// --- CHART INITIALIZATION ---
|
| 233 |
+
|
| 234 |
+
function initCharts() {
|
| 235 |
+
// Time Domain Chart
|
| 236 |
+
const timeCtx = document.getElementById('timeDomainChart').getContext('2d');
|
| 237 |
+
timeDomainChart = new Chart(timeCtx, {
|
| 238 |
+
type: 'line',
|
| 239 |
+
data: {
|
| 240 |
+
datasets: [
|
| 241 |
+
{
|
| 242 |
+
label: 'Composite Signal',
|
| 243 |
+
data: [],
|
| 244 |
+
borderColor: '#1976d2',
|
| 245 |
+
backgroundColor: 'rgba(25, 118, 210, 0.1)',
|
| 246 |
+
borderWidth: 2,
|
| 247 |
+
pointRadius: 0,
|
| 248 |
+
fill: true,
|
| 249 |
+
tension: 0.1
|
| 250 |
+
},
|
| 251 |
+
{
|
| 252 |
+
label: 'Wave 1',
|
| 253 |
+
data: [],
|
| 254 |
+
borderColor: WAVE_COLORS[0],
|
| 255 |
+
borderWidth: 1,
|
| 256 |
+
pointRadius: 0,
|
| 257 |
+
borderDash: [5, 5],
|
| 258 |
+
fill: false,
|
| 259 |
+
tension: 0.1,
|
| 260 |
+
hidden: false
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
label: 'Wave 2',
|
| 264 |
+
data: [],
|
| 265 |
+
borderColor: WAVE_COLORS[1],
|
| 266 |
+
borderWidth: 1,
|
| 267 |
+
pointRadius: 0,
|
| 268 |
+
borderDash: [5, 5],
|
| 269 |
+
fill: false,
|
| 270 |
+
tension: 0.1,
|
| 271 |
+
hidden: false
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
label: 'Wave 3',
|
| 275 |
+
data: [],
|
| 276 |
+
borderColor: WAVE_COLORS[2],
|
| 277 |
+
borderWidth: 1,
|
| 278 |
+
pointRadius: 0,
|
| 279 |
+
borderDash: [5, 5],
|
| 280 |
+
fill: false,
|
| 281 |
+
tension: 0.1,
|
| 282 |
+
hidden: true
|
| 283 |
+
},
|
| 284 |
+
{
|
| 285 |
+
label: 'Wave 4',
|
| 286 |
+
data: [],
|
| 287 |
+
borderColor: WAVE_COLORS[3],
|
| 288 |
+
borderWidth: 1,
|
| 289 |
+
pointRadius: 0,
|
| 290 |
+
borderDash: [5, 5],
|
| 291 |
+
fill: false,
|
| 292 |
+
tension: 0.1,
|
| 293 |
+
hidden: true
|
| 294 |
+
}
|
| 295 |
+
]
|
| 296 |
+
},
|
| 297 |
+
options: {
|
| 298 |
+
responsive: true,
|
| 299 |
+
maintainAspectRatio: false,
|
| 300 |
+
animation: { duration: 0 },
|
| 301 |
+
interaction: {
|
| 302 |
+
mode: 'index',
|
| 303 |
+
intersect: false
|
| 304 |
+
},
|
| 305 |
+
scales: {
|
| 306 |
+
x: {
|
| 307 |
+
type: 'linear',
|
| 308 |
+
title: { display: true, text: 'Time (s)' }
|
| 309 |
+
},
|
| 310 |
+
y: {
|
| 311 |
+
title: { display: true, text: 'Amplitude' },
|
| 312 |
+
min: -25,
|
| 313 |
+
max: 25
|
| 314 |
+
}
|
| 315 |
+
},
|
| 316 |
+
plugins: {
|
| 317 |
+
legend: {
|
| 318 |
+
display: true,
|
| 319 |
+
labels: {
|
| 320 |
+
filter: function(item, data) {
|
| 321 |
+
// Only show legend for visible waves
|
| 322 |
+
const dataset = data.datasets[item.datasetIndex];
|
| 323 |
+
return !dataset.hidden;
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
},
|
| 327 |
+
tooltip: {
|
| 328 |
+
callbacks: {
|
| 329 |
+
title: function(context) {
|
| 330 |
+
return `t = ${context[0].parsed.x.toFixed(4)} s`;
|
| 331 |
+
}
|
| 332 |
+
}
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
});
|
| 337 |
+
|
| 338 |
+
// Magnitude Spectrum Chart
|
| 339 |
+
const magCtx = document.getElementById('magnitudeChart').getContext('2d');
|
| 340 |
+
magnitudeChart = new Chart(magCtx, {
|
| 341 |
+
type: 'bar',
|
| 342 |
+
data: {
|
| 343 |
+
datasets: [{
|
| 344 |
+
label: 'Magnitude',
|
| 345 |
+
data: [],
|
| 346 |
+
backgroundColor: 'rgba(25, 118, 210, 0.7)',
|
| 347 |
+
borderColor: '#1976d2',
|
| 348 |
+
borderWidth: 1
|
| 349 |
+
}]
|
| 350 |
+
},
|
| 351 |
+
options: {
|
| 352 |
+
responsive: true,
|
| 353 |
+
maintainAspectRatio: false,
|
| 354 |
+
animation: { duration: 0 },
|
| 355 |
+
scales: {
|
| 356 |
+
x: {
|
| 357 |
+
type: 'linear',
|
| 358 |
+
title: { display: true, text: 'Frequency (Hz)' },
|
| 359 |
+
min: 0
|
| 360 |
+
},
|
| 361 |
+
y: {
|
| 362 |
+
title: { display: true, text: '|X(f)|' },
|
| 363 |
+
beginAtZero: true
|
| 364 |
+
}
|
| 365 |
+
},
|
| 366 |
+
plugins: {
|
| 367 |
+
legend: { display: false },
|
| 368 |
+
tooltip: {
|
| 369 |
+
callbacks: {
|
| 370 |
+
title: function(context) {
|
| 371 |
+
return `f = ${context[0].parsed.x.toFixed(1)} Hz`;
|
| 372 |
+
},
|
| 373 |
+
label: function(context) {
|
| 374 |
+
return `Magnitude: ${context.parsed.y.toFixed(3)}`;
|
| 375 |
+
}
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
});
|
| 381 |
+
|
| 382 |
+
// Phase Spectrum Chart
|
| 383 |
+
const phaseCtx = document.getElementById('phaseChart').getContext('2d');
|
| 384 |
+
phaseChart = new Chart(phaseCtx, {
|
| 385 |
+
type: 'scatter',
|
| 386 |
+
data: {
|
| 387 |
+
datasets: [{
|
| 388 |
+
label: 'Phase',
|
| 389 |
+
data: [],
|
| 390 |
+
backgroundColor: 'rgba(46, 125, 50, 0.7)',
|
| 391 |
+
borderColor: '#2e7d32',
|
| 392 |
+
borderWidth: 1,
|
| 393 |
+
pointRadius: 4
|
| 394 |
+
}]
|
| 395 |
+
},
|
| 396 |
+
options: {
|
| 397 |
+
responsive: true,
|
| 398 |
+
maintainAspectRatio: false,
|
| 399 |
+
animation: { duration: 0 },
|
| 400 |
+
scales: {
|
| 401 |
+
x: {
|
| 402 |
+
type: 'linear',
|
| 403 |
+
title: { display: true, text: 'Frequency (Hz)' },
|
| 404 |
+
min: 0
|
| 405 |
+
},
|
| 406 |
+
y: {
|
| 407 |
+
title: { display: true, text: 'Phase (degrees)' },
|
| 408 |
+
min: -180,
|
| 409 |
+
max: 180,
|
| 410 |
+
ticks: {
|
| 411 |
+
stepSize: 45
|
| 412 |
+
}
|
| 413 |
+
}
|
| 414 |
+
},
|
| 415 |
+
plugins: {
|
| 416 |
+
legend: { display: false },
|
| 417 |
+
tooltip: {
|
| 418 |
+
callbacks: {
|
| 419 |
+
title: function(context) {
|
| 420 |
+
return `f = ${context[0].parsed.x.toFixed(1)} Hz`;
|
| 421 |
+
},
|
| 422 |
+
label: function(context) {
|
| 423 |
+
return `Phase: ${context.parsed.y.toFixed(1)}°`;
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
}
|
| 427 |
+
}
|
| 428 |
+
}
|
| 429 |
+
});
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
// --- UPDATE FUNCTIONS ---
|
| 433 |
+
|
| 434 |
+
function updateVisualizations() {
|
| 435 |
+
const { time, signal, individualWaves, numSamples } = generateCompositeSignal();
|
| 436 |
+
|
| 437 |
+
// Update Time Domain Chart
|
| 438 |
+
timeDomainChart.data.datasets[0].data = signal.map((y, i) => ({ x: time[i], y }));
|
| 439 |
+
|
| 440 |
+
// Update individual wave datasets
|
| 441 |
+
for (let i = 0; i < 4; i++) {
|
| 442 |
+
const waveData = individualWaves.find(w => w.color === WAVE_COLORS[i]);
|
| 443 |
+
if (waveData) {
|
| 444 |
+
timeDomainChart.data.datasets[i + 1].data = waveData.data.map((y, j) => ({ x: time[j], y }));
|
| 445 |
+
timeDomainChart.data.datasets[i + 1].hidden = false;
|
| 446 |
+
} else {
|
| 447 |
+
timeDomainChart.data.datasets[i + 1].data = [];
|
| 448 |
+
timeDomainChart.data.datasets[i + 1].hidden = true;
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
// Compute FFT (falls back to DFT if not power of 2)
|
| 453 |
+
const dftResult = fft(signal);
|
| 454 |
+
const magnitudes = computeMagnitudeSpectrum(dftResult);
|
| 455 |
+
const phases = computePhaseSpectrum(dftResult);
|
| 456 |
+
|
| 457 |
+
// Frequency bins
|
| 458 |
+
const freqResolution = SAMPLING_RATE / numSamples;
|
| 459 |
+
const frequencies = magnitudes.map((_, k) => k * freqResolution);
|
| 460 |
+
const nyquistFreq = SAMPLING_RATE / 2;
|
| 461 |
+
|
| 462 |
+
// Update Magnitude Spectrum
|
| 463 |
+
// Only show frequencies up to Nyquist, and only show significant peaks
|
| 464 |
+
const maxFreq = Math.min(50, nyquistFreq); // Limit display to 50 Hz for better visualization
|
| 465 |
+
const filteredFreqIndices = frequencies
|
| 466 |
+
.map((f, i) => ({ f, i }))
|
| 467 |
+
.filter(item => item.f <= maxFreq);
|
| 468 |
+
|
| 469 |
+
magnitudeChart.data.datasets[0].data = filteredFreqIndices.map(item => ({
|
| 470 |
+
x: item.f,
|
| 471 |
+
y: magnitudes[item.i]
|
| 472 |
+
}));
|
| 473 |
+
magnitudeChart.options.scales.x.max = maxFreq;
|
| 474 |
+
|
| 475 |
+
// Update Phase Spectrum
|
| 476 |
+
const significantPhases = filteredFreqIndices
|
| 477 |
+
.filter(item => magnitudes[item.i] > 0.1) // Only show phase for significant frequencies
|
| 478 |
+
.map(item => ({
|
| 479 |
+
x: item.f,
|
| 480 |
+
y: phases[item.i]
|
| 481 |
+
}));
|
| 482 |
+
|
| 483 |
+
phaseChart.data.datasets[0].data = significantPhases;
|
| 484 |
+
phaseChart.options.scales.x.max = maxFreq;
|
| 485 |
+
|
| 486 |
+
// Update charts
|
| 487 |
+
timeDomainChart.update('none');
|
| 488 |
+
magnitudeChart.update('none');
|
| 489 |
+
phaseChart.update('none');
|
| 490 |
+
|
| 491 |
+
// Update signal information
|
| 492 |
+
updateSignalInfo(numSamples, freqResolution, signal, magnitudes);
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
function updateSignalInfo(numSamples, freqResolution, signal, magnitudes) {
|
| 496 |
+
const nyquistFreq = SAMPLING_RATE / 2;
|
| 497 |
+
const duration = numSamples / SAMPLING_RATE;
|
| 498 |
+
|
| 499 |
+
// Calculate total power (Parseval's theorem)
|
| 500 |
+
const totalPower = magnitudes.reduce((sum, mag, i) => {
|
| 501 |
+
// DC and Nyquist components are not doubled
|
| 502 |
+
const factor = (i === 0 || (i === magnitudes.length - 1 && numSamples % 2 === 0)) ? 1 : 0.5;
|
| 503 |
+
return sum + factor * mag * mag;
|
| 504 |
+
}, 0);
|
| 505 |
+
|
| 506 |
+
// Calculate RMS amplitude
|
| 507 |
+
const rmsAmplitude = Math.sqrt(signal.reduce((sum, val) => sum + val * val, 0) / signal.length);
|
| 508 |
+
|
| 509 |
+
document.getElementById('samplingRate').textContent = `${SAMPLING_RATE} Hz`;
|
| 510 |
+
document.getElementById('nyquistFreq').textContent = `${nyquistFreq} Hz`;
|
| 511 |
+
document.getElementById('freqResolution').textContent = `${freqResolution.toFixed(3)} Hz`;
|
| 512 |
+
document.getElementById('signalDuration').textContent = `${duration.toFixed(3)} s`;
|
| 513 |
+
document.getElementById('totalPower').textContent = totalPower.toFixed(3);
|
| 514 |
+
document.getElementById('rmsAmplitude').textContent = rmsAmplitude.toFixed(3);
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
// --- EVENT LISTENERS ---
|
| 518 |
+
|
| 519 |
+
function setupEventListeners() {
|
| 520 |
+
// Wave enable checkboxes
|
| 521 |
+
for (let i = 1; i <= 4; i++) {
|
| 522 |
+
document.getElementById(`enableWave${i}`).addEventListener('change', function() {
|
| 523 |
+
const waveSection = this.closest('.wave-section');
|
| 524 |
+
if (this.checked) {
|
| 525 |
+
waveSection.classList.remove('disabled');
|
| 526 |
+
} else {
|
| 527 |
+
waveSection.classList.add('disabled');
|
| 528 |
+
}
|
| 529 |
+
updateVisualizations();
|
| 530 |
+
});
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
// Wave parameter sliders
|
| 534 |
+
for (let i = 1; i <= 4; i++) {
|
| 535 |
+
document.getElementById(`freq${i}`).addEventListener('input', function() {
|
| 536 |
+
document.getElementById(`freq${i}Value`).textContent = `${this.value} Hz`;
|
| 537 |
+
updateVisualizations();
|
| 538 |
+
});
|
| 539 |
+
|
| 540 |
+
document.getElementById(`amp${i}`).addEventListener('input', function() {
|
| 541 |
+
document.getElementById(`amp${i}Value`).textContent = parseFloat(this.value).toFixed(1);
|
| 542 |
+
updateVisualizations();
|
| 543 |
+
});
|
| 544 |
+
|
| 545 |
+
document.getElementById(`phase${i}`).addEventListener('input', function() {
|
| 546 |
+
document.getElementById(`phase${i}Value`).textContent = `${this.value}°`;
|
| 547 |
+
updateVisualizations();
|
| 548 |
+
});
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
// Number of samples slider
|
| 552 |
+
document.getElementById('numSamples').addEventListener('input', function() {
|
| 553 |
+
document.getElementById('numSamplesValue').textContent = this.value;
|
| 554 |
+
updateVisualizations();
|
| 555 |
+
});
|
| 556 |
+
|
| 557 |
+
// Noise toggle
|
| 558 |
+
document.getElementById('addNoise').addEventListener('change', function() {
|
| 559 |
+
const noiseLevelGroup = document.getElementById('noiseLevelGroup');
|
| 560 |
+
const noiseLevelSlider = document.getElementById('noiseLevel');
|
| 561 |
+
|
| 562 |
+
if (this.checked) {
|
| 563 |
+
noiseLevelGroup.style.opacity = '1';
|
| 564 |
+
noiseLevelSlider.disabled = false;
|
| 565 |
+
} else {
|
| 566 |
+
noiseLevelGroup.style.opacity = '0.4';
|
| 567 |
+
noiseLevelSlider.disabled = true;
|
| 568 |
+
}
|
| 569 |
+
updateVisualizations();
|
| 570 |
+
});
|
| 571 |
+
|
| 572 |
+
// Noise level slider
|
| 573 |
+
document.getElementById('noiseLevel').addEventListener('input', function() {
|
| 574 |
+
document.getElementById('noiseLevelValue').textContent = parseFloat(this.value).toFixed(1);
|
| 575 |
+
updateVisualizations();
|
| 576 |
+
});
|
| 577 |
+
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
// --- MAIN INITIALIZATION ---
|
| 581 |
+
|
| 582 |
+
function main() {
|
| 583 |
+
initCharts();
|
| 584 |
+
setupEventListeners();
|
| 585 |
+
|
| 586 |
+
// Make controls draggable on desktop
|
| 587 |
+
if (window.innerWidth > 1200) {
|
| 588 |
+
makeDraggable(document.getElementById('floatingControls'), document.getElementById('controlsTitle'));
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
// Initial visualization
|
| 592 |
+
updateVisualizations();
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
window.addEventListener('load', main);
|
src/js/inverse_classifier.js
CHANGED
|
@@ -1,49 +1,10 @@
|
|
| 1 |
// --- GLOBAL VARIABLES ---
|
| 2 |
let scoresChart, rocChart, metricsChart;
|
| 3 |
-
const metricExplanations = {
|
| 4 |
-
'AUC': {
|
| 5 |
-
description: "Measures the model's ability to distinguish between positive and negative classes. It represents the probability that a random positive instance is ranked higher than a random negative instance.",
|
| 6 |
-
range: "Ranges from 0 (worst) to 1 (best). 0.5 is random chance.",
|
| 7 |
-
formula: "Area Under the ROC Curve"
|
| 8 |
-
},
|
| 9 |
-
'Accuracy': {
|
| 10 |
-
description: "The proportion of all predictions that are correct. It's a general measure of the model's performance.",
|
| 11 |
-
range: "Ranges from 0 (worst) to 1 (best).",
|
| 12 |
-
formula: "(TP + TN) / (TP + TN + FP + FN)"
|
| 13 |
-
},
|
| 14 |
-
'Precision': {
|
| 15 |
-
description: "Of all the positive predictions made by the model, how many were actually positive. High precision indicates a low false positive rate.",
|
| 16 |
-
range: "Ranges from 0 (worst) to 1 (best).",
|
| 17 |
-
formula: "TP / (TP + FP)"
|
| 18 |
-
},
|
| 19 |
-
'Recall': {
|
| 20 |
-
description: "Of all the actual positive instances, how many did the model correctly identify. Also known as Sensitivity or True Positive Rate.",
|
| 21 |
-
range: "Ranges from 0 (worst) to 1 (best).",
|
| 22 |
-
formula: "TP / (TP + FN)"
|
| 23 |
-
},
|
| 24 |
-
'Specificity': {
|
| 25 |
-
description: "Of all the actual negative instances, how many did the model correctly identify. Also known as True Negative Rate.",
|
| 26 |
-
range: "Ranges from 0 (worst) to 1 (best).",
|
| 27 |
-
formula: "TN / (TN + FP)"
|
| 28 |
-
},
|
| 29 |
-
'F1-Score': {
|
| 30 |
-
description: "The harmonic mean of Precision and Recall. It provides a single score that balances both concerns, useful for imbalanced classes.",
|
| 31 |
-
range: "Ranges from 0 (worst) to 1 (best).",
|
| 32 |
-
formula: "2 * (Precision * Recall) / (Precision + Recall)"
|
| 33 |
-
}
|
| 34 |
-
};
|
| 35 |
let lockState = { tp: false, fp: false, tn: false, fn: false };
|
| 36 |
let currentState = { tp: 40, fp: 10, tn: 45, fn: 5 };
|
| 37 |
const TOTAL_SAMPLES = 100;
|
| 38 |
|
| 39 |
-
// --- SIMULATION
|
| 40 |
-
function randomGaussian(mean = 0, stdDev = 1) {
|
| 41 |
-
let u = 0, v = 0;
|
| 42 |
-
while (u === 0) u = Math.random();
|
| 43 |
-
while (v === 0) v = Math.random();
|
| 44 |
-
return mean + stdDev * Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
function generateScoresFromMatrix(tp, fp, tn, fn) {
|
| 48 |
const total_pos_real = tp + fn;
|
| 49 |
const total_neg_real = tn + fp;
|
|
@@ -59,26 +20,6 @@ function generateScoresFromMatrix(tp, fp, tn, fn) {
|
|
| 59 |
return { scores, labels };
|
| 60 |
}
|
| 61 |
|
| 62 |
-
function calculateRocAndAuc(labels, scores) {
|
| 63 |
-
const pairs = labels.map((label, i) => ({ label, score: scores[i] }));
|
| 64 |
-
pairs.sort((a, b) => b.score - a.score);
|
| 65 |
-
let tp = 0, fp = 0;
|
| 66 |
-
const total_pos = labels.filter(l => l === 1).length;
|
| 67 |
-
const total_neg = labels.length - total_pos;
|
| 68 |
-
if (total_pos === 0 || total_neg === 0) return { rocPoints: [{ x: 0, y: 0 }, { x: 1, y: 1 }], auc: 0.5 };
|
| 69 |
-
const rocPoints = [{ x: 0, y: 0 }];
|
| 70 |
-
let auc = 0, prev_tpr = 0, prev_fpr = 0;
|
| 71 |
-
for (const pair of pairs) {
|
| 72 |
-
if (pair.label === 1) tp++; else fp++;
|
| 73 |
-
const tpr = tp / total_pos;
|
| 74 |
-
const fpr = fp / total_neg;
|
| 75 |
-
auc += (tpr + prev_tpr) / 2 * (fpr - prev_fpr);
|
| 76 |
-
rocPoints.push({ x: fpr, y: tpr });
|
| 77 |
-
prev_tpr = tpr; prev_fpr = fpr;
|
| 78 |
-
}
|
| 79 |
-
return { rocPoints, auc };
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
function createHistogramData(scores, labels, n_bins = 20) {
|
| 83 |
const bins = Array(n_bins).fill(0).map(() => ({ pos: 0, neg: 0 }));
|
| 84 |
const bin_labels = Array(n_bins).fill(0).map((_, i) => (i / n_bins).toFixed(2));
|
|
@@ -92,43 +33,6 @@ function createHistogramData(scores, labels, n_bins = 20) {
|
|
| 92 |
return { labels: bin_labels, pos_data: bins.map(b => b.pos), neg_data: bins.map(b => b.neg) };
|
| 93 |
}
|
| 94 |
|
| 95 |
-
// [CORRIGÉ] Fonction de dessin utilisant la palette "Blues"
|
| 96 |
-
function drawConfusionMatrix(canvasId, tp, fp, tn, fn) {
|
| 97 |
-
const canvas = document.getElementById(canvasId);
|
| 98 |
-
const ctx = canvas.getContext('2d');
|
| 99 |
-
const w = canvas.width, h = canvas.height;
|
| 100 |
-
ctx.clearRect(0, 0, w, h);
|
| 101 |
-
const margin = 50, gridW = w - margin, gridH = h - margin, cellW = gridW / 2, cellH = gridH / 2;
|
| 102 |
-
const max_val = Math.max(tp, fp, tn, fn);
|
| 103 |
-
const baseColor = [8, 48, 107]; // "Blues" palette base color
|
| 104 |
-
const cells = [
|
| 105 |
-
{ label: 'TN', value: tn, x: 0, y: cellH },
|
| 106 |
-
{ label: 'FP', value: fp, x: cellW, y: cellH },
|
| 107 |
-
{ label: 'FN', value: fn, x: 0, y: 0 },
|
| 108 |
-
{ label: 'TP', value: tp, x: cellW, y: 0 }
|
| 109 |
-
];
|
| 110 |
-
cells.forEach(cell => {
|
| 111 |
-
const intensity = max_val > 0 ? cell.value / max_val : 0;
|
| 112 |
-
ctx.fillStyle = `rgba(${baseColor[0]}, ${baseColor[1]}, ${baseColor[2]}, ${intensity})`;
|
| 113 |
-
ctx.fillRect(margin + cell.x, cell.y, cellW, cellH);
|
| 114 |
-
ctx.fillStyle = intensity > 0.5 ? 'white' : 'black';
|
| 115 |
-
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
| 116 |
-
ctx.font = 'bold 20px Segoe UI'; ctx.fillText(cell.label, margin + cell.x + cellW / 2, cell.y + cellH / 2 - 12);
|
| 117 |
-
ctx.font = '18px Segoe UI'; ctx.fillText(cell.value, margin + cell.x + cellW / 2, cell.y + cellH / 2 + 12);
|
| 118 |
-
});
|
| 119 |
-
ctx.fillStyle = '#333'; ctx.font = 'bold 14px Segoe UI';
|
| 120 |
-
ctx.fillText('Negative', margin + cellW / 2, gridH + 20);
|
| 121 |
-
ctx.fillText('Positive', margin + cellW + cellW / 2, gridH + 20);
|
| 122 |
-
ctx.save();
|
| 123 |
-
ctx.translate(20, gridH / 2);
|
| 124 |
-
ctx.rotate(-Math.PI / 2);
|
| 125 |
-
ctx.textAlign = 'center';
|
| 126 |
-
ctx.textBaseline = 'middle';
|
| 127 |
-
ctx.fillText('Positive', -cellH / 2, 0);
|
| 128 |
-
ctx.fillText('Negative', cellH / 2, 0);
|
| 129 |
-
ctx.restore();
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
// --- UI MANAGEMENT ---
|
| 133 |
function adjustValues(changedParam, newValue) {
|
| 134 |
let values = { ...currentState };
|
|
@@ -182,28 +86,6 @@ function updateUI() {
|
|
| 182 |
}
|
| 183 |
|
| 184 |
// --- INITIALIZATION ---
|
| 185 |
-
const customDatalabelsPlugin = {
|
| 186 |
-
id: 'customDatalabels',
|
| 187 |
-
afterDatasetsDraw: (chart) => {
|
| 188 |
-
const ctx = chart.ctx;
|
| 189 |
-
ctx.save();
|
| 190 |
-
ctx.font = 'bold 12px Segoe UI';
|
| 191 |
-
ctx.fillStyle = 'white';
|
| 192 |
-
ctx.textAlign = 'center';
|
| 193 |
-
chart.data.datasets.forEach((dataset, i) => {
|
| 194 |
-
const meta = chart.getDatasetMeta(i);
|
| 195 |
-
meta.data.forEach((bar, index) => {
|
| 196 |
-
const data = dataset.data[index];
|
| 197 |
-
if (bar.height > 15) {
|
| 198 |
-
ctx.textBaseline = 'bottom';
|
| 199 |
-
ctx.fillText(data.toFixed(3), bar.x, bar.y + bar.height - 5);
|
| 200 |
-
}
|
| 201 |
-
});
|
| 202 |
-
});
|
| 203 |
-
ctx.restore();
|
| 204 |
-
}
|
| 205 |
-
};
|
| 206 |
-
|
| 207 |
function initCharts() {
|
| 208 |
const scoresCtx = document.getElementById('scoresChart').getContext('2d');
|
| 209 |
scoresChart = new Chart(scoresCtx, { type: 'bar', data: { labels: [], datasets: [{ label: 'Scores (Negative Class)', data: [], backgroundColor: '#0D47A1', barPercentage: 1.0, categoryPercentage: 1.0 }, { label: 'Scores (Positive Class)', data: [], backgroundColor: '#B71C1C', barPercentage: 1.0, categoryPercentage: 1.0 }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0 }, scales: { x: { stacked: true }, y: { stacked: true, title: { display: true, text: 'Number of Samples' } } } } });
|
|
@@ -232,20 +114,7 @@ function initCharts() {
|
|
| 232 |
padding: 15,
|
| 233 |
displayColors: false,
|
| 234 |
callbacks: {
|
| 235 |
-
label:
|
| 236 |
-
const label = context.chart.data.labels[context.dataIndex];
|
| 237 |
-
const value = context.raw.toFixed(3);
|
| 238 |
-
const explanation = metricExplanations[label];
|
| 239 |
-
let tooltipText = [`${label}: ${value}`];
|
| 240 |
-
if (explanation) {
|
| 241 |
-
tooltipText.push('');
|
| 242 |
-
const roleLines = `Role: ${explanation.description}`.match(/.{1,50}(\s|$)/g) || [];
|
| 243 |
-
roleLines.forEach(line => tooltipText.push(line.trim()));
|
| 244 |
-
tooltipText.push(`Range: ${explanation.range}`);
|
| 245 |
-
tooltipText.push(`Formula: ${explanation.formula}`);
|
| 246 |
-
}
|
| 247 |
-
return tooltipText;
|
| 248 |
-
}
|
| 249 |
}
|
| 250 |
}
|
| 251 |
},
|
|
@@ -260,13 +129,6 @@ function updateSliderDisabledState() {
|
|
| 260 |
for (const param in lockState) { sliders[param].disabled = lockState[param] || lockedCount >= 3; }
|
| 261 |
}
|
| 262 |
|
| 263 |
-
function makeDraggable(element, handle) {
|
| 264 |
-
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
| 265 |
-
handle.onmousedown = (e) => { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; };
|
| 266 |
-
const elementDrag = (e) => { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; element.style.top = (element.offsetTop - pos2) + "px"; element.style.left = (element.offsetLeft - pos1) + "px"; };
|
| 267 |
-
const closeDragElement = () => { document.onmouseup = null; document.onmousemove = null; };
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
window.addEventListener('load', function () {
|
| 271 |
initCharts();
|
| 272 |
Object.keys(currentState).forEach(param => {
|
|
|
|
| 1 |
// --- GLOBAL VARIABLES ---
|
| 2 |
let scoresChart, rocChart, metricsChart;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
let lockState = { tp: false, fp: false, tn: false, fn: false };
|
| 4 |
let currentState = { tp: 40, fp: 10, tn: 45, fn: 5 };
|
| 5 |
const TOTAL_SAMPLES = 100;
|
| 6 |
|
| 7 |
+
// --- SIMULATION ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
function generateScoresFromMatrix(tp, fp, tn, fn) {
|
| 9 |
const total_pos_real = tp + fn;
|
| 10 |
const total_neg_real = tn + fp;
|
|
|
|
| 20 |
return { scores, labels };
|
| 21 |
}
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
function createHistogramData(scores, labels, n_bins = 20) {
|
| 24 |
const bins = Array(n_bins).fill(0).map(() => ({ pos: 0, neg: 0 }));
|
| 25 |
const bin_labels = Array(n_bins).fill(0).map((_, i) => (i / n_bins).toFixed(2));
|
|
|
|
| 33 |
return { labels: bin_labels, pos_data: bins.map(b => b.pos), neg_data: bins.map(b => b.neg) };
|
| 34 |
}
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
// --- UI MANAGEMENT ---
|
| 37 |
function adjustValues(changedParam, newValue) {
|
| 38 |
let values = { ...currentState };
|
|
|
|
| 86 |
}
|
| 87 |
|
| 88 |
// --- INITIALIZATION ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
function initCharts() {
|
| 90 |
const scoresCtx = document.getElementById('scoresChart').getContext('2d');
|
| 91 |
scoresChart = new Chart(scoresCtx, { type: 'bar', data: { labels: [], datasets: [{ label: 'Scores (Negative Class)', data: [], backgroundColor: '#0D47A1', barPercentage: 1.0, categoryPercentage: 1.0 }, { label: 'Scores (Positive Class)', data: [], backgroundColor: '#B71C1C', barPercentage: 1.0, categoryPercentage: 1.0 }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0 }, scales: { x: { stacked: true }, y: { stacked: true, title: { display: true, text: 'Number of Samples' } } } } });
|
|
|
|
| 114 |
padding: 15,
|
| 115 |
displayColors: false,
|
| 116 |
callbacks: {
|
| 117 |
+
label: metricsTooltipCallback
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
}
|
| 119 |
}
|
| 120 |
},
|
|
|
|
| 129 |
for (const param in lockState) { sliders[param].disabled = lockState[param] || lockedCount >= 3; }
|
| 130 |
}
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
window.addEventListener('load', function () {
|
| 133 |
initCharts();
|
| 134 |
Object.keys(currentState).forEach(param => {
|
src/js/linear_regression.js
ADDED
|
@@ -0,0 +1,730 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ============================================================
|
| 2 |
+
// linear_regression.js — Linear Regression Playground
|
| 3 |
+
// ============================================================
|
| 4 |
+
|
| 5 |
+
// --- GLOBAL VARIABLES ---
|
| 6 |
+
let mainChart, residualChart;
|
| 7 |
+
let points = []; // Array of {x, y, id}
|
| 8 |
+
let nextPointId = 0;
|
| 9 |
+
let isDragging = false;
|
| 10 |
+
let draggedPointId = null;
|
| 11 |
+
let regressionCoefficients = null;
|
| 12 |
+
|
| 13 |
+
// Chart scales
|
| 14 |
+
const X_MIN = -10;
|
| 15 |
+
const X_MAX = 10;
|
| 16 |
+
const Y_MIN = -10;
|
| 17 |
+
const Y_MAX = 10;
|
| 18 |
+
|
| 19 |
+
// Zoom state
|
| 20 |
+
let currentXMin = X_MIN;
|
| 21 |
+
let currentXMax = X_MAX;
|
| 22 |
+
let currentYMin = Y_MIN;
|
| 23 |
+
let currentYMax = Y_MAX;
|
| 24 |
+
const ZOOM_FACTOR = 0.1; // 10% zoom per wheel step
|
| 25 |
+
|
| 26 |
+
// --- POLYNOMIAL REGRESSION IMPLEMENTATION ---
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Build Vandermonde matrix for polynomial regression
|
| 30 |
+
* X: array of x values
|
| 31 |
+
* degree: polynomial degree
|
| 32 |
+
*/
|
| 33 |
+
function buildVandermonde(X, degree) {
|
| 34 |
+
const n = X.length;
|
| 35 |
+
const V = [];
|
| 36 |
+
for (let i = 0; i < n; i++) {
|
| 37 |
+
const row = [];
|
| 38 |
+
for (let j = 0; j <= degree; j++) {
|
| 39 |
+
row.push(Math.pow(X[i], j));
|
| 40 |
+
}
|
| 41 |
+
V.push(row);
|
| 42 |
+
}
|
| 43 |
+
return V;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* Transpose a matrix
|
| 48 |
+
*/
|
| 49 |
+
function transpose(matrix) {
|
| 50 |
+
const rows = matrix.length;
|
| 51 |
+
const cols = matrix[0].length;
|
| 52 |
+
const result = [];
|
| 53 |
+
for (let j = 0; j < cols; j++) {
|
| 54 |
+
const row = [];
|
| 55 |
+
for (let i = 0; i < rows; i++) {
|
| 56 |
+
row.push(matrix[i][j]);
|
| 57 |
+
}
|
| 58 |
+
result.push(row);
|
| 59 |
+
}
|
| 60 |
+
return result;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/**
|
| 64 |
+
* Matrix multiplication: A * B
|
| 65 |
+
*/
|
| 66 |
+
function multiply(A, B) {
|
| 67 |
+
const rowsA = A.length;
|
| 68 |
+
const colsA = A[0].length;
|
| 69 |
+
const colsB = B[0].length;
|
| 70 |
+
const result = [];
|
| 71 |
+
for (let i = 0; i < rowsA; i++) {
|
| 72 |
+
const row = [];
|
| 73 |
+
for (let j = 0; j < colsB; j++) {
|
| 74 |
+
let sum = 0;
|
| 75 |
+
for (let k = 0; k < colsA; k++) {
|
| 76 |
+
sum += A[i][k] * B[k][j];
|
| 77 |
+
}
|
| 78 |
+
row.push(sum);
|
| 79 |
+
}
|
| 80 |
+
result.push(row);
|
| 81 |
+
}
|
| 82 |
+
return result;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/**
|
| 86 |
+
* Solve linear system Ax = b using Gaussian elimination
|
| 87 |
+
*/
|
| 88 |
+
function solveLinearSystem(A, b) {
|
| 89 |
+
const n = A.length;
|
| 90 |
+
// Augmented matrix
|
| 91 |
+
const M = A.map((row, i) => [...row, b[i][0]]);
|
| 92 |
+
|
| 93 |
+
// Forward elimination
|
| 94 |
+
for (let i = 0; i < n; i++) {
|
| 95 |
+
// Find pivot
|
| 96 |
+
let maxRow = i;
|
| 97 |
+
for (let k = i + 1; k < n; k++) {
|
| 98 |
+
if (Math.abs(M[k][i]) > Math.abs(M[maxRow][i])) {
|
| 99 |
+
maxRow = k;
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
// Swap rows
|
| 103 |
+
[M[i], M[maxRow]] = [M[maxRow], M[i]];
|
| 104 |
+
|
| 105 |
+
// Make all rows below this one 0 in current column
|
| 106 |
+
for (let k = i + 1; k < n; k++) {
|
| 107 |
+
const factor = M[k][i] / M[i][i];
|
| 108 |
+
for (let j = i; j <= n; j++) {
|
| 109 |
+
M[k][j] -= factor * M[i][j];
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// Back substitution
|
| 115 |
+
const x = new Array(n).fill(0);
|
| 116 |
+
for (let i = n - 1; i >= 0; i--) {
|
| 117 |
+
x[i] = M[i][n] / M[i][i];
|
| 118 |
+
for (let k = i - 1; k >= 0; k--) {
|
| 119 |
+
M[k][n] -= M[k][i] * x[i];
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
return x;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/**
|
| 127 |
+
* Fit polynomial regression model
|
| 128 |
+
* Returns coefficients [β0, β1, ..., βdegree]
|
| 129 |
+
*/
|
| 130 |
+
function fitPolynomialRegression(X, y, degree) {
|
| 131 |
+
if (X.length < degree + 1) {
|
| 132 |
+
return null; // Not enough points
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
const V = buildVandermonde(X, degree);
|
| 136 |
+
const Vt = transpose(V);
|
| 137 |
+
const VtV = multiply(Vt, V);
|
| 138 |
+
const Vty = multiply(Vt, y.map(val => [val]));
|
| 139 |
+
|
| 140 |
+
// Solve (V^T * V) * β = V^T * y
|
| 141 |
+
const coefficients = solveLinearSystem(VtV, Vty);
|
| 142 |
+
return coefficients;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/**
|
| 146 |
+
* Predict y values for given x values using fitted model
|
| 147 |
+
*/
|
| 148 |
+
function predict(X, coefficients) {
|
| 149 |
+
if (!coefficients) return X.map(() => 0);
|
| 150 |
+
return X.map(x => {
|
| 151 |
+
let y = 0;
|
| 152 |
+
for (let i = 0; i < coefficients.length; i++) {
|
| 153 |
+
y += coefficients[i] * Math.pow(x, i);
|
| 154 |
+
}
|
| 155 |
+
return y;
|
| 156 |
+
});
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
/**
|
| 160 |
+
* Calculate regression statistics
|
| 161 |
+
*/
|
| 162 |
+
function calculateStatistics(yTrue, yPred, n, degree) {
|
| 163 |
+
const residuals = yTrue.map((y, i) => y - yPred[i]);
|
| 164 |
+
const ssRes = residuals.reduce((sum, r) => sum + r * r, 0);
|
| 165 |
+
const yMean = yTrue.reduce((sum, y) => sum + y, 0) / yTrue.length;
|
| 166 |
+
const ssTot = yTrue.reduce((sum, y) => sum + Math.pow(y - yMean, 2), 0);
|
| 167 |
+
|
| 168 |
+
const r2 = ssTot > 0 ? 1 - ssRes / ssTot : 0;
|
| 169 |
+
const adjustedR2 = n > degree + 1 ? 1 - (1 - r2) * (n - 1) / (n - degree - 1) : r2;
|
| 170 |
+
const mse = ssRes / n;
|
| 171 |
+
const rmse = Math.sqrt(mse);
|
| 172 |
+
const mae = residuals.reduce((sum, r) => sum + Math.abs(r), 0) / n;
|
| 173 |
+
|
| 174 |
+
return { r2, adjustedR2, mse, rmse, mae, residuals };
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/**
|
| 178 |
+
* Calculate confidence intervals for predictions (pointwise ±2σ)
|
| 179 |
+
*/
|
| 180 |
+
function calculateConfidenceInterval(X, coefficients, residuals, confidence = 0.95) {
|
| 181 |
+
if (!coefficients || X.length === 0) return X.map(() => ({ lower: 0, upper: 0 }));
|
| 182 |
+
|
| 183 |
+
const n = residuals.length;
|
| 184 |
+
const degree = coefficients.length - 1;
|
| 185 |
+
|
| 186 |
+
// Estimate standard deviation of residuals
|
| 187 |
+
const ssRes = residuals.reduce((sum, r) => sum + r * r, 0);
|
| 188 |
+
const sigma = Math.sqrt(ssRes / Math.max(1, n - degree - 1));
|
| 189 |
+
|
| 190 |
+
// For simplicity, use ±2σ as approximate 95% confidence interval
|
| 191 |
+
const multiplier = 2;
|
| 192 |
+
|
| 193 |
+
return X.map(x => {
|
| 194 |
+
const yPred = predict([x], coefficients)[0];
|
| 195 |
+
return {
|
| 196 |
+
lower: yPred - multiplier * sigma,
|
| 197 |
+
upper: yPred + multiplier * sigma
|
| 198 |
+
};
|
| 199 |
+
});
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// --- CHART INITIALIZATION ---
|
| 203 |
+
|
| 204 |
+
function initCharts() {
|
| 205 |
+
// Main chart (scatter + regression line)
|
| 206 |
+
const mainCtx = document.getElementById('mainChart').getContext('2d');
|
| 207 |
+
mainChart = new Chart(mainCtx, {
|
| 208 |
+
type: 'scatter',
|
| 209 |
+
data: {
|
| 210 |
+
datasets: [
|
| 211 |
+
{
|
| 212 |
+
label: 'Data Points',
|
| 213 |
+
data: [],
|
| 214 |
+
backgroundColor: '#1976d2',
|
| 215 |
+
borderColor: '#1976d2',
|
| 216 |
+
borderWidth: 2,
|
| 217 |
+
pointRadius: 6,
|
| 218 |
+
pointHoverRadius: 8,
|
| 219 |
+
pointHoverBorderWidth: 3
|
| 220 |
+
},
|
| 221 |
+
{
|
| 222 |
+
label: 'Regression Line',
|
| 223 |
+
data: [],
|
| 224 |
+
type: 'line',
|
| 225 |
+
borderColor: '#d32f2f',
|
| 226 |
+
backgroundColor: 'transparent',
|
| 227 |
+
borderWidth: 2,
|
| 228 |
+
pointRadius: 0,
|
| 229 |
+
fill: false,
|
| 230 |
+
tension: 0.4
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
label: 'Confidence Band Lower',
|
| 234 |
+
data: [],
|
| 235 |
+
type: 'line',
|
| 236 |
+
borderColor: 'transparent',
|
| 237 |
+
backgroundColor: 'rgba(211, 47, 47, 0.1)',
|
| 238 |
+
borderWidth: 0,
|
| 239 |
+
pointRadius: 0,
|
| 240 |
+
fill: '+1'
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
label: 'Confidence Band Upper',
|
| 244 |
+
data: [],
|
| 245 |
+
type: 'line',
|
| 246 |
+
borderColor: 'transparent',
|
| 247 |
+
backgroundColor: 'rgba(211, 47, 47, 0.1)',
|
| 248 |
+
borderWidth: 0,
|
| 249 |
+
pointRadius: 0,
|
| 250 |
+
fill: false
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
label: 'Residuals',
|
| 254 |
+
data: [],
|
| 255 |
+
type: 'line',
|
| 256 |
+
borderColor: '#757575',
|
| 257 |
+
backgroundColor: 'transparent',
|
| 258 |
+
borderWidth: 1,
|
| 259 |
+
borderDash: [3, 3],
|
| 260 |
+
pointRadius: 0,
|
| 261 |
+
showLine: true
|
| 262 |
+
}
|
| 263 |
+
]
|
| 264 |
+
},
|
| 265 |
+
options: {
|
| 266 |
+
responsive: true,
|
| 267 |
+
maintainAspectRatio: false,
|
| 268 |
+
animation: { duration: 0 },
|
| 269 |
+
scales: {
|
| 270 |
+
x: {
|
| 271 |
+
min: X_MIN,
|
| 272 |
+
max: X_MAX,
|
| 273 |
+
title: { display: true, text: 'X' }
|
| 274 |
+
},
|
| 275 |
+
y: {
|
| 276 |
+
min: Y_MIN,
|
| 277 |
+
max: Y_MAX,
|
| 278 |
+
title: { display: true, text: 'Y' }
|
| 279 |
+
}
|
| 280 |
+
},
|
| 281 |
+
plugins: {
|
| 282 |
+
legend: {
|
| 283 |
+
display: true,
|
| 284 |
+
labels: {
|
| 285 |
+
filter: function(item) {
|
| 286 |
+
// Hide confidence band entries from legend
|
| 287 |
+
return item.text !== 'Confidence Band Lower' && item.text !== 'Confidence Band Upper';
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
},
|
| 291 |
+
tooltip: {
|
| 292 |
+
callbacks: {
|
| 293 |
+
label: function(context) {
|
| 294 |
+
if (context.dataset.label === 'Data Points') {
|
| 295 |
+
return `Point (${context.parsed.x.toFixed(2)}, ${context.parsed.y.toFixed(2)})`;
|
| 296 |
+
}
|
| 297 |
+
return `${context.dataset.label}: ${context.parsed.y.toFixed(3)}`;
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
// Note: onClick and onHover removed - using native events instead
|
| 303 |
+
}
|
| 304 |
+
});
|
| 305 |
+
|
| 306 |
+
// Residual chart
|
| 307 |
+
const residualCtx = document.getElementById('residualChart').getContext('2d');
|
| 308 |
+
residualChart = new Chart(residualCtx, {
|
| 309 |
+
type: 'scatter',
|
| 310 |
+
data: {
|
| 311 |
+
datasets: [
|
| 312 |
+
{
|
| 313 |
+
label: 'Residuals',
|
| 314 |
+
data: [],
|
| 315 |
+
backgroundColor: '#1976d2',
|
| 316 |
+
pointRadius: 5
|
| 317 |
+
},
|
| 318 |
+
{
|
| 319 |
+
label: 'Zero Line',
|
| 320 |
+
data: [{ x: Y_MIN, y: 0 }, { x: Y_MAX, y: 0 }],
|
| 321 |
+
type: 'line',
|
| 322 |
+
borderColor: '#d32f2f',
|
| 323 |
+
borderWidth: 2,
|
| 324 |
+
pointRadius: 0,
|
| 325 |
+
borderDash: [5, 5]
|
| 326 |
+
}
|
| 327 |
+
]
|
| 328 |
+
},
|
| 329 |
+
options: {
|
| 330 |
+
responsive: true,
|
| 331 |
+
maintainAspectRatio: false,
|
| 332 |
+
animation: { duration: 0 },
|
| 333 |
+
scales: {
|
| 334 |
+
x: {
|
| 335 |
+
title: { display: true, text: 'Predicted Values' }
|
| 336 |
+
},
|
| 337 |
+
y: {
|
| 338 |
+
title: { display: true, text: 'Residuals' }
|
| 339 |
+
}
|
| 340 |
+
},
|
| 341 |
+
plugins: {
|
| 342 |
+
legend: { display: false }
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
});
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
// --- INTERACTION HANDLERS ---
|
| 349 |
+
|
| 350 |
+
function findNearestPoint(x, y, threshold = 1.0) {
|
| 351 |
+
for (const point of points) {
|
| 352 |
+
const dist = Math.sqrt(Math.pow(point.x - x, 2) + Math.pow(point.y - y, 2));
|
| 353 |
+
if (dist < threshold) {
|
| 354 |
+
return point;
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
return null;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
function getMousePos(canvas, event) {
|
| 361 |
+
const rect = canvas.getBoundingClientRect();
|
| 362 |
+
return {
|
| 363 |
+
x: event.clientX - rect.left,
|
| 364 |
+
y: event.clientY - rect.top
|
| 365 |
+
};
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
function getChartCoordinates(canvas, event) {
|
| 369 |
+
const pos = getMousePos(canvas, event);
|
| 370 |
+
return {
|
| 371 |
+
x: mainChart.scales.x.getValueForPixel(pos.x),
|
| 372 |
+
y: mainChart.scales.y.getValueForPixel(pos.y)
|
| 373 |
+
};
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
function addPoint(x, y) {
|
| 377 |
+
// Clamp values to current chart bounds (allows adding points outside initial view when zoomed out)
|
| 378 |
+
x = Math.max(currentXMin, Math.min(currentXMax, x));
|
| 379 |
+
y = Math.max(currentYMin, Math.min(currentYMax, y));
|
| 380 |
+
|
| 381 |
+
points.push({ x, y, id: nextPointId++ });
|
| 382 |
+
updateRegression();
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
function removePoint(id) {
|
| 386 |
+
points = points.filter(p => p.id !== id);
|
| 387 |
+
updateRegression();
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
function updatePointPosition(id, x, y) {
|
| 391 |
+
const point = points.find(p => p.id === id);
|
| 392 |
+
if (point) {
|
| 393 |
+
point.x = Math.max(currentXMin, Math.min(currentXMax, x));
|
| 394 |
+
point.y = Math.max(currentYMin, Math.min(currentYMax, y));
|
| 395 |
+
updateRegression();
|
| 396 |
+
}
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
// --- ZOOM FUNCTIONS ---
|
| 400 |
+
|
| 401 |
+
function zoom(factor, centerX, centerY) {
|
| 402 |
+
const xRange = currentXMax - currentXMin;
|
| 403 |
+
const yRange = currentYMax - currentYMin;
|
| 404 |
+
|
| 405 |
+
// Calculate new ranges
|
| 406 |
+
const newXRange = xRange * factor;
|
| 407 |
+
const newYRange = yRange * factor;
|
| 408 |
+
|
| 409 |
+
// Calculate ratios for center point
|
| 410 |
+
const xRatio = (centerX - currentXMin) / xRange;
|
| 411 |
+
const yRatio = (centerY - currentYMin) / yRange;
|
| 412 |
+
|
| 413 |
+
// Calculate new bounds keeping the center point stable
|
| 414 |
+
currentXMin = centerX - newXRange * xRatio;
|
| 415 |
+
currentXMax = centerX + newXRange * (1 - xRatio);
|
| 416 |
+
currentYMin = centerY - newYRange * yRatio;
|
| 417 |
+
currentYMax = centerY + newYRange * (1 - yRatio);
|
| 418 |
+
|
| 419 |
+
// Update chart scales
|
| 420 |
+
mainChart.options.scales.x.min = currentXMin;
|
| 421 |
+
mainChart.options.scales.x.max = currentXMax;
|
| 422 |
+
mainChart.options.scales.y.min = currentYMin;
|
| 423 |
+
mainChart.options.scales.y.max = currentYMax;
|
| 424 |
+
mainChart.update('none');
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
function resetZoom() {
|
| 428 |
+
if (points.length === 0) {
|
| 429 |
+
// No points: reset to default view
|
| 430 |
+
currentXMin = X_MIN;
|
| 431 |
+
currentXMax = X_MAX;
|
| 432 |
+
currentYMin = Y_MIN;
|
| 433 |
+
currentYMax = Y_MAX;
|
| 434 |
+
} else {
|
| 435 |
+
// Fit to content: show all points with padding
|
| 436 |
+
const xs = points.map(p => p.x);
|
| 437 |
+
const ys = points.map(p => p.y);
|
| 438 |
+
|
| 439 |
+
const xMin = Math.min(...xs);
|
| 440 |
+
const xMax = Math.max(...xs);
|
| 441 |
+
const yMin = Math.min(...ys);
|
| 442 |
+
const yMax = Math.max(...ys);
|
| 443 |
+
|
| 444 |
+
// Add padding (20% of range or minimum 2 units)
|
| 445 |
+
const xRange = xMax - xMin;
|
| 446 |
+
const yRange = yMax - yMin;
|
| 447 |
+
const xPadding = Math.max(xRange * 0.1, 1);
|
| 448 |
+
const yPadding = Math.max(yRange * 0.1, 1);
|
| 449 |
+
|
| 450 |
+
currentXMin = xMin - xPadding;
|
| 451 |
+
currentXMax = xMax + xPadding;
|
| 452 |
+
currentYMin = yMin - yPadding;
|
| 453 |
+
currentYMax = yMax + yPadding;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
mainChart.options.scales.x.min = currentXMin;
|
| 457 |
+
mainChart.options.scales.x.max = currentXMax;
|
| 458 |
+
mainChart.options.scales.y.min = currentYMin;
|
| 459 |
+
mainChart.options.scales.y.max = currentYMax;
|
| 460 |
+
mainChart.update('none');
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
// --- UPDATE REGRESSION AND UI ---
|
| 464 |
+
|
| 465 |
+
function updateRegression() {
|
| 466 |
+
const degree = parseInt(document.getElementById('degreeSlider').value);
|
| 467 |
+
const showResiduals = document.getElementById('showResiduals').checked;
|
| 468 |
+
const showConfidence = document.getElementById('showConfidence').checked;
|
| 469 |
+
|
| 470 |
+
// Update main chart data points
|
| 471 |
+
mainChart.data.datasets[0].data = points.map(p => ({ x: p.x, y: p.y }));
|
| 472 |
+
|
| 473 |
+
if (points.length < degree + 1) {
|
| 474 |
+
// Not enough points for regression
|
| 475 |
+
mainChart.data.datasets[1].data = [];
|
| 476 |
+
mainChart.data.datasets[2].data = [];
|
| 477 |
+
mainChart.data.datasets[3].data = [];
|
| 478 |
+
mainChart.data.datasets[4].data = [];
|
| 479 |
+
residualChart.data.datasets[0].data = [];
|
| 480 |
+
updateStatisticsDisplay({ r2: 0, adjustedR2: 0, mse: 0, rmse: 0, mae: 0 }, points.length);
|
| 481 |
+
mainChart.update('none');
|
| 482 |
+
residualChart.update('none');
|
| 483 |
+
return;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
// Fit regression model
|
| 487 |
+
const X = points.map(p => p.x);
|
| 488 |
+
const y = points.map(p => p.y);
|
| 489 |
+
regressionCoefficients = fitPolynomialRegression(X, y, degree);
|
| 490 |
+
|
| 491 |
+
// Generate smooth curve for visualization
|
| 492 |
+
const xCurve = [];
|
| 493 |
+
for (let x = currentXMin; x <= currentXMax; x += 0.2) {
|
| 494 |
+
xCurve.push(x);
|
| 495 |
+
}
|
| 496 |
+
const yCurve = predict(xCurve, regressionCoefficients);
|
| 497 |
+
|
| 498 |
+
// Update regression line
|
| 499 |
+
mainChart.data.datasets[1].data = xCurve.map((x, i) => ({ x, y: yCurve[i] }));
|
| 500 |
+
|
| 501 |
+
// Calculate predictions for actual points
|
| 502 |
+
const yPred = predict(X, regressionCoefficients);
|
| 503 |
+
const stats = calculateStatistics(y, yPred, points.length, degree);
|
| 504 |
+
|
| 505 |
+
// Update confidence band
|
| 506 |
+
if (showConfidence) {
|
| 507 |
+
const confidenceIntervals = calculateConfidenceInterval(xCurve, regressionCoefficients, stats.residuals);
|
| 508 |
+
mainChart.data.datasets[2].data = xCurve.map((x, i) => ({ x, y: confidenceIntervals[i].lower }));
|
| 509 |
+
mainChart.data.datasets[3].data = xCurve.map((x, i) => ({ x, y: confidenceIntervals[i].upper }));
|
| 510 |
+
} else {
|
| 511 |
+
mainChart.data.datasets[2].data = [];
|
| 512 |
+
mainChart.data.datasets[3].data = [];
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
// Update residual lines
|
| 516 |
+
if (showResiduals) {
|
| 517 |
+
const residualLines = [];
|
| 518 |
+
for (let i = 0; i < points.length; i++) {
|
| 519 |
+
residualLines.push({ x: X[i], y: y[i] });
|
| 520 |
+
residualLines.push({ x: X[i], y: yPred[i] });
|
| 521 |
+
residualLines.push({ x: NaN, y: NaN }); // Break line
|
| 522 |
+
}
|
| 523 |
+
mainChart.data.datasets[4].data = residualLines;
|
| 524 |
+
} else {
|
| 525 |
+
mainChart.data.datasets[4].data = [];
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
// Update residual chart
|
| 529 |
+
residualChart.data.datasets[0].data = yPred.map((yp, i) => ({ x: yp, y: stats.residuals[i] }));
|
| 530 |
+
|
| 531 |
+
// Update statistics display
|
| 532 |
+
updateStatisticsDisplay(stats, points.length);
|
| 533 |
+
|
| 534 |
+
mainChart.update('none');
|
| 535 |
+
residualChart.update('none');
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
function updateStatisticsDisplay(stats, n) {
|
| 539 |
+
document.getElementById('r2Value').textContent = n > 0 ? stats.r2.toFixed(4) : '-';
|
| 540 |
+
document.getElementById('adjustedR2Value').textContent = n > 0 ? stats.adjustedR2.toFixed(4) : '-';
|
| 541 |
+
document.getElementById('mseValue').textContent = n > 0 ? stats.mse.toFixed(4) : '-';
|
| 542 |
+
document.getElementById('maeValue').textContent = n > 0 ? stats.mae.toFixed(4) : '-';
|
| 543 |
+
document.getElementById('rmseValue').textContent = n > 0 ? stats.rmse.toFixed(4) : '-';
|
| 544 |
+
document.getElementById('nPointsValue').textContent = n;
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
// --- EVENT LISTENERS ---
|
| 548 |
+
|
| 549 |
+
function setupEventListeners() {
|
| 550 |
+
// Degree slider
|
| 551 |
+
const degreeSlider = document.getElementById('degreeSlider');
|
| 552 |
+
degreeSlider.addEventListener('input', function() {
|
| 553 |
+
document.getElementById('degreeValue').textContent = this.value;
|
| 554 |
+
updateRegression();
|
| 555 |
+
});
|
| 556 |
+
|
| 557 |
+
// Show residuals toggle
|
| 558 |
+
document.getElementById('showResiduals').addEventListener('change', updateRegression);
|
| 559 |
+
|
| 560 |
+
// Show confidence toggle
|
| 561 |
+
document.getElementById('showConfidence').addEventListener('change', updateRegression);
|
| 562 |
+
|
| 563 |
+
// Clear points button
|
| 564 |
+
document.getElementById('clearPointsBtn').addEventListener('click', function() {
|
| 565 |
+
points = [];
|
| 566 |
+
updateRegression();
|
| 567 |
+
});
|
| 568 |
+
|
| 569 |
+
// Add random points button
|
| 570 |
+
document.getElementById('addRandomBtn').addEventListener('click', function() {
|
| 571 |
+
for (let i = 0; i < 5; i++) {
|
| 572 |
+
const x = currentXMin + Math.random() * (currentXMax - currentXMin);
|
| 573 |
+
const y = (currentYMin + Math.random() * (currentYMax - currentYMin)) + randomGaussian(0, 2);
|
| 574 |
+
addPoint(x, y);
|
| 575 |
+
}
|
| 576 |
+
});
|
| 577 |
+
|
| 578 |
+
// Reset zoom button
|
| 579 |
+
document.getElementById('resetZoomBtn').addEventListener('click', resetZoom);
|
| 580 |
+
|
| 581 |
+
// Canvas interactions
|
| 582 |
+
const canvas = document.getElementById('mainChart');
|
| 583 |
+
let hasDragged = false;
|
| 584 |
+
let clickedOnPoint = false;
|
| 585 |
+
|
| 586 |
+
// Canvas for zoom with mouse wheel
|
| 587 |
+
canvas.addEventListener('wheel', function(event) {
|
| 588 |
+
event.preventDefault();
|
| 589 |
+
|
| 590 |
+
const rect = canvas.getBoundingClientRect();
|
| 591 |
+
const mouseX = event.clientX - rect.left;
|
| 592 |
+
const mouseY = event.clientY - rect.top;
|
| 593 |
+
|
| 594 |
+
// Get chart coordinates of mouse position
|
| 595 |
+
const centerX = mainChart.scales.x.getValueForPixel(mouseX);
|
| 596 |
+
const centerY = mainChart.scales.y.getValueForPixel(mouseY);
|
| 597 |
+
|
| 598 |
+
// Zoom in (scroll up, deltaY < 0) or zoom out (scroll down, deltaY > 0)
|
| 599 |
+
const zoomDirection = event.deltaY < 0 ? (1 - ZOOM_FACTOR) : (1 + ZOOM_FACTOR);
|
| 600 |
+
zoom(zoomDirection, centerX, centerY);
|
| 601 |
+
}, { passive: false });
|
| 602 |
+
|
| 603 |
+
// Mouse down - start drag or prepare for click
|
| 604 |
+
canvas.addEventListener('mousedown', function(event) {
|
| 605 |
+
if (event.button !== 0) return; // Only left click
|
| 606 |
+
|
| 607 |
+
event.preventDefault();
|
| 608 |
+
event.stopPropagation();
|
| 609 |
+
|
| 610 |
+
const coords = getChartCoordinates(canvas, event);
|
| 611 |
+
const nearestPoint = findNearestPoint(coords.x, coords.y);
|
| 612 |
+
|
| 613 |
+
hasDragged = false;
|
| 614 |
+
|
| 615 |
+
if (nearestPoint) {
|
| 616 |
+
// Clicked on existing point - start dragging
|
| 617 |
+
isDragging = true;
|
| 618 |
+
draggedPointId = nearestPoint.id;
|
| 619 |
+
clickedOnPoint = true;
|
| 620 |
+
canvas.classList.add('dragging');
|
| 621 |
+
} else {
|
| 622 |
+
// Clicked on empty space - prepare for adding point
|
| 623 |
+
clickedOnPoint = false;
|
| 624 |
+
}
|
| 625 |
+
});
|
| 626 |
+
|
| 627 |
+
// Mouse move - handle dragging
|
| 628 |
+
document.addEventListener('mousemove', function(event) {
|
| 629 |
+
// Update cursor style on hover when not dragging
|
| 630 |
+
if (!isDragging) {
|
| 631 |
+
const coords = getChartCoordinates(canvas, event);
|
| 632 |
+
if (coords.x >= X_MIN && coords.x <= X_MAX && coords.y >= Y_MIN && coords.y <= Y_MAX) {
|
| 633 |
+
const nearestPoint = findNearestPoint(coords.x, coords.y);
|
| 634 |
+
if (nearestPoint) {
|
| 635 |
+
canvas.classList.add('hover-point');
|
| 636 |
+
} else {
|
| 637 |
+
canvas.classList.remove('hover-point');
|
| 638 |
+
}
|
| 639 |
+
}
|
| 640 |
+
return;
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
// We are dragging
|
| 644 |
+
hasDragged = true;
|
| 645 |
+
|
| 646 |
+
const rect = canvas.getBoundingClientRect();
|
| 647 |
+
let canvasX = event.clientX - rect.left;
|
| 648 |
+
let canvasY = event.clientY - rect.top;
|
| 649 |
+
|
| 650 |
+
// Clamp to canvas bounds
|
| 651 |
+
canvasX = Math.max(0, Math.min(canvasX, rect.width));
|
| 652 |
+
canvasY = Math.max(0, Math.min(canvasY, rect.height));
|
| 653 |
+
|
| 654 |
+
const x = mainChart.scales.x.getValueForPixel(canvasX);
|
| 655 |
+
const y = mainChart.scales.y.getValueForPixel(canvasY);
|
| 656 |
+
|
| 657 |
+
updatePointPosition(draggedPointId, x, y);
|
| 658 |
+
});
|
| 659 |
+
|
| 660 |
+
// Mouse up - end drag or handle click
|
| 661 |
+
document.addEventListener('mouseup', function(event) {
|
| 662 |
+
if (event.button !== 0) return; // Only left click
|
| 663 |
+
|
| 664 |
+
if (isDragging) {
|
| 665 |
+
// End drag
|
| 666 |
+
isDragging = false;
|
| 667 |
+
draggedPointId = null;
|
| 668 |
+
canvas.classList.remove('dragging');
|
| 669 |
+
} else if (!clickedOnPoint) {
|
| 670 |
+
// This was a click on empty space - add point
|
| 671 |
+
const coords = getChartCoordinates(canvas, event);
|
| 672 |
+
// Check if click is within current visible bounds (not just initial bounds)
|
| 673 |
+
if (coords.x >= currentXMin && coords.x <= currentXMax && coords.y >= currentYMin && coords.y <= currentYMax) {
|
| 674 |
+
addPoint(coords.x, coords.y);
|
| 675 |
+
}
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
// Reset flags
|
| 679 |
+
hasDragged = false;
|
| 680 |
+
clickedOnPoint = false;
|
| 681 |
+
});
|
| 682 |
+
|
| 683 |
+
// Right-click to remove point
|
| 684 |
+
canvas.addEventListener('contextmenu', function(event) {
|
| 685 |
+
event.preventDefault();
|
| 686 |
+
event.stopPropagation();
|
| 687 |
+
|
| 688 |
+
if (isDragging) {
|
| 689 |
+
// Cancel drag
|
| 690 |
+
isDragging = false;
|
| 691 |
+
draggedPointId = null;
|
| 692 |
+
canvas.classList.remove('dragging');
|
| 693 |
+
return;
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
const coords = getChartCoordinates(canvas, event);
|
| 697 |
+
const clickedPoint = findNearestPoint(coords.x, coords.y);
|
| 698 |
+
if (clickedPoint) {
|
| 699 |
+
removePoint(clickedPoint.id);
|
| 700 |
+
}
|
| 701 |
+
});
|
| 702 |
+
|
| 703 |
+
// Prevent context menu on right-click
|
| 704 |
+
canvas.addEventListener('contextmenu', function(event) {
|
| 705 |
+
event.preventDefault();
|
| 706 |
+
});
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
// --- MAIN INITIALIZATION ---
|
| 710 |
+
|
| 711 |
+
function main() {
|
| 712 |
+
initCharts();
|
| 713 |
+
setupEventListeners();
|
| 714 |
+
|
| 715 |
+
// Make controls draggable on desktop
|
| 716 |
+
if (window.innerWidth > 1200) {
|
| 717 |
+
makeDraggable(document.getElementById('floatingControls'), document.getElementById('controlsTitle'));
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
// Add some initial random points
|
| 721 |
+
for (let i = 0; i < 8; i++) {
|
| 722 |
+
const x = X_MIN + Math.random() * (X_MAX - X_MIN);
|
| 723 |
+
const y = 2 + 0.5 * x + randomGaussian(0, 1.5);
|
| 724 |
+
points.push({ x, y, id: nextPointId++ });
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
updateRegression();
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
window.addEventListener('load', main);
|