berangerthomas commited on
Commit
09edf62
·
1 Parent(s): 4e4036b

Add Fourier & linear pages; unify styles

Browse files

Add 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 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 - Interactive Classification Dashboards
12
-
13
- ![Logo](./src/assets/logo.jpg)
14
 
15
- Welcome to the Interactive Classification Dashboards project! This repository contains a set of tools designed to help users understand the core concepts of binary classification in machine learning through hands-on, visual interaction.
16
 
17
- ## 🚀 About This Project
18
 
19
- ### 🌐 Live Demos
20
 
21
- * **[Direct Classification Dashboard](https://berangerthomas.github.io/SchoolOfStatistics/direct_classifier.html)**
22
- * **[Inverse Classification Dashboard](https://berangerthomas.github.io/SchoolOfStatistics/inverse_classifier.html)**
 
 
23
 
24
- This project provides two distinct interactive dashboards:
25
 
26
- 1. **Direct Classification Dashboard (`direct_classifier.html`)** : This tool allows you to generate a synthetic 2D dataset for two classes. You can adjust the **class separation** and **data spread (standard deviation)** to see how these parameters affect the performance of a Gaussian Naive Bayes classifier. The dashboard visualizes:
27
- * The generated data points.
28
- * The resulting ROC curve and its Area Under the Curve (AUC).
29
- * Key performance metrics (Accuracy, Precision, Recall, etc.).
30
- * A detailed confusion matrix.
31
 
32
- 2. **Inverse Classification Dashboard (`inverse_classifier.html`)** : This tool works in reverse. Instead of generating data, you directly manipulate the values of the **confusion matrix** (True Positives, False Positives, True Negatives, and False Negatives). The application then simulates a distribution of classifier scores that would lead to your specified matrix and visualizes the resulting metrics, ROC curve, and score distribution. This provides a unique, intuitive way to understand the relationships between the confusion matrix and other performance indicators.
33
 
34
- ## 📂 Project Structure
 
 
35
 
36
- The project has been organized into a clean and maintainable structure:
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
- │ ├── inverse_style.css
49
- └── style.css
50
- └── js
 
 
 
 
 
51
  ├── direct_classifier.js
52
- └── inverse_classifier.js
 
 
 
 
53
  ```
54
 
55
- * **`direct_classifier.html`**: The main page for the direct classification tool.
56
- * **`inverse_classifier.html`**: The main page for the inverse classification tool.
57
- * **`src/`**: Contains all source assets.
58
- * **`assets/`**: Stores static assets like the project logo.
59
- * **`css/`**: Contains the stylesheets for the HTML pages.
60
- * **`js/`**: Contains the JavaScript logic for each interactive dashboard.
61
- * **`LICENSE`**: The project's license file.
62
- * **`README.md`**: This file.
 
 
 
 
 
 
63
 
64
- ## 🛠️ How to Use
65
 
66
- 1. Clone this repository to your local machine.
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
- Interact with the sliders and controls on each page to explore the concepts of classification.
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- ## 📄 License
73
 
74
- This project is distributed under the terms of the license specified in the `LICENSE` file.
 
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 (), 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>Pro 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>Pro 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,15 +31,15 @@
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,7 +49,7 @@
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,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>Interactive 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/inverse_style.css">
10
  </head>
11
 
12
  <body>
13
  <div class="container">
14
- <h1>Interactive Classification Dashboard</h1>
15
 
16
  <div class="floating-controls" id="floatingControls">
17
- <div class="controls-title" id="controlsTitle">🎛️ Result 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,15 +45,15 @@
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,7 +63,7 @@
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,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', 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
 
@@ -54,84 +117,195 @@ h1 {
54
  .controls-grid {
55
  display: grid;
56
  grid-template-columns: 1fr;
57
- gap: 20px;
 
 
 
 
 
 
58
  }
59
 
 
60
  .control-group {
61
- text-align: center;
62
  }
63
 
64
  .control-label {
65
- font-weight: bold;
66
- margin-bottom: 5px;
67
- font-size: 14px;
68
- color: #555;
 
 
 
 
 
 
 
 
 
 
 
 
69
  }
70
 
 
 
 
 
 
 
 
 
 
 
71
  .slider {
72
  width: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
 
75
  .slider-value {
76
- font-weight: bold;
77
- color: #333;
78
- margin-top: 5px;
79
- font-size: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  }
81
 
 
82
  .charts-container {
83
  display: grid;
84
- grid-template-columns: 1fr 1fr;
85
- gap: 30px;
 
86
  }
87
 
 
88
  .chart-card {
89
- background: rgba(255, 255, 255, 0.95);
90
- border-radius: 15px;
91
- padding: 25px;
92
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
 
 
 
 
 
 
93
  }
94
 
95
  .chart-title {
96
- font-size: 18px;
97
- font-weight: bold;
98
- margin-bottom: 15px;
99
  text-align: center;
100
- color: #333;
 
 
101
  }
102
 
103
  .chart-container {
104
  position: relative;
105
- height: 300px;
106
- margin-bottom: 20px;
107
  }
108
 
 
109
  .confusion-matrix-layout {
110
  position: relative;
111
  width: 250px;
112
  height: 250px;
113
- margin: 0 auto; /* Center the block horizontally */
114
- top: 50%; /* Center the block vertically */
115
  transform: translateY(-50%);
116
  }
117
 
118
  .matrix-ylabel,
119
  .matrix-xlabel {
120
  position: absolute;
121
- font-weight: bold;
122
- color: #555;
123
- font-style: italic;
 
 
124
  white-space: nowrap;
125
  }
126
 
127
  .matrix-ylabel {
128
  top: 50%;
129
- left: -20px; /* Position label just outside the container */
130
  transform: translateY(-50%) rotate(-90deg);
131
  }
132
 
133
  .matrix-xlabel {
134
- bottom: -25px; /* Position label just outside the container */
135
  left: 50%;
136
  transform: translateX(-50%);
137
  }
@@ -146,32 +320,194 @@ h1 {
146
  max-height: 100%;
147
  }
148
 
149
- /* [NOUVEAU] Style pour le footer */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  footer {
151
  text-align: center;
152
- padding: 20px;
153
- margin-top: 40px;
 
 
154
  }
155
 
156
  footer a {
157
- color: rgba(255, 255, 255, 0.8);
158
  text-decoration: none;
159
- font-weight: bold;
160
- transition: color 0.3s;
 
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
- margin-bottom: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 vp = 0, fp = 0, vn = 0, fn = 0;
111
  labels.forEach((label, i) => {
112
  const prediction = scores[i] >= threshold ? 1 : 0;
113
- if (prediction === 1 && label === 1) vp++;
114
  else if (prediction === 1 && label === 0) fp++;
115
- else if (prediction === 0 && label === 0) vn++;
116
  else if (prediction === 0 && label === 1) fn++;
117
  });
118
- return { vp, fp, vn, fn };
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 { vp, fp, vn, fn } = getConfusionMatrix(labels, scores, 0.5);
165
- const total = vp + fp + vn + fn;
166
- const precision = (vp + fp) > 0 ? vp / (vp + fp) : 0;
167
- const recall = (vp + fn) > 0 ? vp / (vp + fn) : 0;
168
- const specificity = (vn + fp) > 0 ? vn / (vn + fp) : 0;
169
  const f1score = (precision + recall) > 0 ? 2 * (precision * recall) / (precision + recall) : 0;
170
- const accuracy = total > 0 ? (vp + vn) / total : 0;
171
 
172
- drawConfusionMatrix('matrixChart', vp, fp, vn, fn);
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: function (context) {
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 & CALCULATIONS ---
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: function (context) {
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);