berangerthomas commited on
Commit
41cacc0
·
1 Parent(s): 5d64457

Refactor: Reorganize project structure and add README

Browse files
README.md CHANGED
@@ -1,2 +1,59 @@
1
- # SchoolOfStatistics
2
- SOS ! School Of Statistics - From confusion matrices to crystal clarity: your statistical rescue toolkit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # School of Statistics - Interactive Classification Dashboards
2
+
3
+ ![Logo](./src/assets/logo.jpg)
4
+
5
+ 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.
6
+
7
+ ## 🚀 About This Project
8
+
9
+ This project provides two distinct interactive dashboards:
10
+
11
+ 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:
12
+ * The generated data points.
13
+ * The resulting ROC curve and its Area Under the Curve (AUC).
14
+ * Key performance metrics (Accuracy, Precision, Recall, etc.).
15
+ * A detailed confusion matrix.
16
+
17
+ 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.
18
+
19
+ ## 📂 Project Structure
20
+
21
+ The project has been organized into a clean and maintainable structure:
22
+
23
+ ```
24
+ .
25
+ ├── direct_classifier.html
26
+ ├── inverse_classifier.html
27
+ ├── LICENSE
28
+ ├── README.md
29
+ └── src
30
+ ├── assets
31
+ │ └── logo.jpg
32
+ ├── css
33
+ │ ├── inverse_style.css
34
+ │ └── style.css
35
+ └── js
36
+ ├── direct_classifier.js
37
+ └── inverse_classifier.js
38
+ ```
39
+
40
+ * **`direct_classifier.html`**: The main page for the direct classification tool.
41
+ * **`inverse_classifier.html`**: The main page for the inverse classification tool.
42
+ * **`src/`**: Contains all source assets.
43
+ * **`assets/`**: Stores static assets like the project logo.
44
+ * **`css/`**: Contains the stylesheets for the HTML pages.
45
+ * **`js/`**: Contains the JavaScript logic for each interactive dashboard.
46
+ * **`LICENSE`**: The project's license file.
47
+ * **`README.md`**: This file.
48
+
49
+ ## 🛠️ How to Use
50
+
51
+ 1. Clone this repository to your local machine.
52
+ 2. Open either `direct_classifier.html` or `inverse_classifier.html` in your web browser.
53
+ 3. No local server is needed! All the logic is self-contained in the HTML, CSS, and JavaScript files.
54
+
55
+ Interact with the sliders and controls on each page to explore the concepts of classification.
56
+
57
+ ## 📄 License
58
+
59
+ This project is distributed under the terms of the license specified in the `LICENSE` file.
direct_classifier.html ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>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>
21
+ <input type="range" min="0" max="10" value="3" step="0.1" class="slider" id="separationSlider">
22
+ <div class="slider-value" id="separationValue">3.0</div>
23
+ </div>
24
+ <div class="control-group">
25
+ <label for="stdDevSlider" class="control-label">Class Spread (Std. Dev.)</label>
26
+ <input type="range" min="0.5" max="5" value="1" step="0.1" class="slider" id="stdDevSlider">
27
+ <div class="slider-value" id="stdDevValue">1.0</div>
28
+ </div>
29
+ </div>
30
+ </div>
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="matrix-wrapper">
44
+ <div class="matrix-ylabel">Each row corresponds to the TRUE class</div>
45
+ <div class="matrix-main">
46
+ <canvas id="matrixChart" width="350" height="350"></canvas>
47
+ <div class="matrix-xlabel">Each column corresponds to the PREDICTED class</div>
48
+ </div>
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>
56
+ </div>
57
+
58
+ <!-- Footer -->
59
+ <footer>
60
+ <a href="https://github.com/berangerthomas/SchoolOfStatistics" target="_blank"
61
+ rel="noopener noreferrer">Project's GitHub</a>
62
+ </footer>
63
+
64
+ <script src="src/js/direct_classifier.js"></script>
65
+ </body>
66
+
67
+ </html>
inverse_classifier.html ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>V5.2 - 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>V5.2 - 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">True Negatives (TN) <span class="lock-toggle" data-param="tn">🔓</span>
21
+ </div>
22
+ <input type="range" min="0" max="200" value="80" class="slider" id="tnSlider">
23
+ <div class="slider-value" id="tnValue">80</div>
24
+ </div>
25
+ <div class="control-group">
26
+ <div class="control-label">False Positives (FP) <span class="lock-toggle" data-param="fp">🔓</span>
27
+ </div>
28
+ <input type="range" min="0" max="200" value="20" class="slider" id="fpSlider">
29
+ <div class="slider-value" id="fpValue">20</div>
30
+ </div>
31
+ <div class="control-group">
32
+ <div class="control-label">False Negatives (FN) <span class="lock-toggle" data-param="fn">🔓</span>
33
+ </div>
34
+ <input type="range" min="0" max="200" value="30" class="slider" id="fnSlider">
35
+ <div class="slider-value" id="fnValue">30</div>
36
+ </div>
37
+ <div class="control-group">
38
+ <div class="control-label">True Positives (TP) <span class="lock-toggle" data-param="tp">🔓</span>
39
+ </div>
40
+ <input type="range" min="0" max="200" value="70" class="slider" id="tpSlider">
41
+ <div class="slider-value" id="tpValue">70</div>
42
+ </div>
43
+ </div>
44
+ </div>
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="matrix-wrapper">
58
+ <div class="matrix-ylabel">Each row corresponds to the TRUE class</div>
59
+ <div class="matrix-main">
60
+ <canvas id="matrixChart" width="350" height="350"></canvas>
61
+ <div class="matrix-xlabel">Each column corresponds to the PREDICTED class</div>
62
+ </div>
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>
70
+ </div>
71
+
72
+ <!-- Footer -->
73
+ <footer>
74
+ <a href="https://github.com/berangerthomas/SchoolOfStatistics" target="_blank"
75
+ rel="noopener noreferrer">Project's GitHub</a>
76
+ </footer>
77
+
78
+ <script src="src/js/inverse_classifier.js"></script>
79
+ </body>
80
+
81
+ </html>
src/assets/logo.jpg ADDED

Git LFS Details

  • SHA256: ddae57be70254206d49390f3a177cf2da9d5c6f47d71406e8ec5e2ebd899d864
  • Pointer size: 131 Bytes
  • Size of remote file: 120 kB
src/css/inverse_style.css ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ display: flex;
169
+ justify-content: center;
170
+ align-items: center;
171
+ }
172
+
173
+ .matrix-wrapper {
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: center;
177
+ width: 100%;
178
+ height: 100%;
179
+ flex-direction: row;
180
+ }
181
+
182
+ .matrix-ylabel {
183
+ writing-mode: vertical-rl;
184
+ transform: rotate(180deg);
185
+ text-align: center;
186
+ margin-right: 15px;
187
+ color: #555;
188
+ font-style: italic;
189
+ }
190
+
191
+ .matrix-xlabel {
192
+ text-align: center;
193
+ margin-top: 15px;
194
+ color: #555;
195
+ font-style: italic;
196
+ }
197
+
198
+ #matrixChart {
199
+ max-width: 100%;
200
+ max-height: 100%;
201
+ }
202
+
203
+ footer {
204
+ text-align: center;
205
+ padding: 20px;
206
+ margin-top: 40px;
207
+ }
208
+
209
+ footer a {
210
+ color: rgba(255, 255, 255, 0.8);
211
+ text-decoration: none;
212
+ font-weight: bold;
213
+ transition: color 0.3s;
214
+ }
215
+
216
+ footer a:hover {
217
+ color: white;
218
+ }
219
+
220
+ @media (max-width: 1200px) {
221
+ .charts-container {
222
+ grid-template-columns: 1fr;
223
+ }
224
+
225
+ .floating-controls {
226
+ position: static;
227
+ width: 100%;
228
+ margin-bottom: 20px;
229
+ }
230
+ }
src/css/style.css ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ .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
+ display: flex;
108
+ justify-content: center;
109
+ align-items: center;
110
+ }
111
+
112
+ .matrix-wrapper {
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: center;
116
+ width: 100%;
117
+ height: 100%;
118
+ flex-direction: row;
119
+ }
120
+
121
+ .matrix-ylabel {
122
+ writing-mode: vertical-rl;
123
+ transform: rotate(180deg);
124
+ text-align: center;
125
+ margin-right: 15px;
126
+ color: #555;
127
+ font-style: italic;
128
+ }
129
+
130
+ .matrix-xlabel {
131
+ text-align: center;
132
+ margin-top: 15px;
133
+ color: #555;
134
+ font-style: italic;
135
+ }
136
+
137
+ #matrixChart {
138
+ max-width: 100%;
139
+ max-height: 100%;
140
+ }
141
+
142
+ /* [NOUVEAU] Style pour le footer */
143
+ footer {
144
+ text-align: center;
145
+ padding: 20px;
146
+ margin-top: 40px;
147
+ }
148
+
149
+ footer a {
150
+ color: rgba(255, 255, 255, 0.8);
151
+ text-decoration: none;
152
+ font-weight: bold;
153
+ transition: color 0.3s;
154
+ }
155
+
156
+ footer a:hover {
157
+ color: white;
158
+ }
159
+
160
+ @media (max-width: 1200px) {
161
+ .charts-container {
162
+ grid-template-columns: 1fr;
163
+ }
164
+
165
+ .floating-controls {
166
+ position: static;
167
+ width: 100%;
168
+ margin-bottom: 20px;
169
+ }
170
+ }
src/js/direct_classifier.js ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // --- GLOBAL VARIABLES ---
2
+ let dataChart, rocChart, metricsChart;
3
+ const N_SAMPLES_PER_CLASS = 100;
4
+
5
+ // --- DATA GENERATION ---
6
+ function randomGaussian(mean = 0, stdDev = 1) {
7
+ let u = 0, v = 0;
8
+ while (u === 0) u = Math.random();
9
+ while (v === 0) v = Math.random();
10
+ return mean + stdDev * Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
11
+ }
12
+
13
+ function generateData(separation, stdDev) {
14
+ const data = [], labels = [];
15
+ for (let i = 0; i < N_SAMPLES_PER_CLASS; i++) { data.push({ x: randomGaussian(-separation / 2, stdDev), y: randomGaussian(0, stdDev) }); labels.push(0); }
16
+ for (let i = 0; i < N_SAMPLES_PER_CLASS; i++) { data.push({ x: randomGaussian(separation / 2, stdDev), y: randomGaussian(0, stdDev) }); labels.push(1); }
17
+ return { data, labels };
18
+ }
19
+
20
+ // --- CLASSIFIER: GAUSSIAN NAIVE BAYES ---
21
+ class GaussianNB {
22
+ fit(X, y) {
23
+ const classes = [...new Set(y)];
24
+ this.classes = classes;
25
+ this.params = {};
26
+ for (const cls of classes) {
27
+ const X_cls = X.filter((_, i) => y[i] === cls);
28
+ const mean_x = X_cls.reduce((a, b) => a + b.x, 0) / X_cls.length;
29
+ const mean_y = X_cls.reduce((a, b) => a + b.y, 0) / X_cls.length;
30
+ this.params[cls] = {
31
+ prior: X_cls.length / X.length,
32
+ mean: [mean_x, mean_y],
33
+ variance: [Math.max(1e-9, X_cls.reduce((a, b) => a + Math.pow(b.x - mean_x, 2), 0) / X_cls.length), Math.max(1e-9, X_cls.reduce((a, b) => a + Math.pow(b.y - mean_y, 2), 0) / X_cls.length)]
34
+ };
35
+ }
36
+ }
37
+ _pdf(x, mean, variance) { const exponent = Math.exp(-Math.pow(x - mean, 2) / (2 * variance)); return (1 / Math.sqrt(2 * Math.PI * variance)) * exponent; }
38
+ predict_proba(X) {
39
+ return X.map(point => {
40
+ const posteriors = {};
41
+ for (const cls of this.classes) {
42
+ const prior = Math.log(this.params[cls].prior);
43
+ const likelihood_x = Math.log(this._pdf(point.x, this.params[cls].mean[0], this.params[cls].variance[0]));
44
+ const likelihood_y = Math.log(this._pdf(point.y, this.params[cls].mean[1], this.params[cls].variance[1]));
45
+ posteriors[cls] = prior + likelihood_x + likelihood_y;
46
+ }
47
+ const max_posterior = Math.max(...Object.values(posteriors));
48
+ const exps = Object.fromEntries(Object.entries(posteriors).map(([k, v]) => [k, Math.exp(v - max_posterior)]));
49
+ const sum_exps = Object.values(exps).reduce((a, b) => a + b);
50
+ return exps[1] / sum_exps;
51
+ });
52
+ }
53
+ }
54
+
55
+ // --- METRICS CALCULATIONS ---
56
+ function calculateRocAndAuc(labels, scores) {
57
+ const pairs = labels.map((label, i) => ({ label, score: scores[i] }));
58
+ pairs.sort((a, b) => b.score - a.score);
59
+ let tp = 0, fp = 0;
60
+ const total_pos = labels.filter(l => l === 1).length;
61
+ const total_neg = labels.length - total_pos;
62
+ if (total_pos === 0 || total_neg === 0) return { rocPoints: [{ x: 0, y: 0 }, { x: 1, y: 1 }], auc: 0.5 };
63
+ const rocPoints = [{ x: 0, y: 0 }];
64
+ let auc = 0, prev_tpr = 0, prev_fpr = 0;
65
+ for (const pair of pairs) {
66
+ if (pair.label === 1) tp++; else fp++;
67
+ const tpr = tp / total_pos;
68
+ const fpr = fp / total_neg;
69
+ auc += (tpr + prev_tpr) / 2 * (fpr - prev_fpr);
70
+ rocPoints.push({ x: fpr, y: tpr });
71
+ prev_tpr = tpr; prev_fpr = fpr;
72
+ }
73
+ return { rocPoints, auc };
74
+ }
75
+
76
+ function getConfusionMatrix(labels, scores, threshold) {
77
+ let vp = 0, fp = 0, vn = 0, fn = 0;
78
+ labels.forEach((label, i) => {
79
+ const prediction = scores[i] >= threshold ? 1 : 0;
80
+ if (prediction === 1 && label === 1) vp++;
81
+ else if (prediction === 1 && label === 0) fp++;
82
+ else if (prediction === 0 && label === 0) vn++;
83
+ else if (prediction === 0 && label === 1) fn++;
84
+ });
85
+ return { vp, fp, vn, fn };
86
+ }
87
+
88
+ function drawConfusionMatrix(canvasId, vp, fp, vn, fn) {
89
+ const canvas = document.getElementById(canvasId);
90
+ const ctx = canvas.getContext('2d');
91
+ const w = canvas.width, h = canvas.height;
92
+ ctx.clearRect(0, 0, w, h);
93
+ const margin = 50, gridW = w - margin, gridH = h - margin, cellW = gridW / 2, cellH = gridH / 2;
94
+ const max_val = Math.max(vp, fp, vn, fn);
95
+ const baseColor = [8, 48, 107];
96
+ 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 }];
97
+ cells.forEach(cell => {
98
+ const intensity = max_val > 0 ? cell.value / max_val : 0;
99
+ ctx.fillStyle = `rgba(${baseColor[0]}, ${baseColor[1]}, ${baseColor[2]}, ${intensity})`;
100
+ ctx.fillRect(margin + cell.x, cell.y, cellW, cellH);
101
+ ctx.fillStyle = intensity > 0.5 ? 'white' : 'black';
102
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
103
+ ctx.font = 'bold 20px Segoe UI'; ctx.fillText(cell.label, margin + cell.x + cellW / 2, cell.y + cellH / 2 - 12);
104
+ ctx.font = '18px Segoe UI'; ctx.fillText(cell.value, margin + cell.x + cellW / 2, cell.y + cellH / 2 + 12);
105
+ });
106
+ ctx.fillStyle = '#333'; ctx.font = 'bold 14px Segoe UI';
107
+ ctx.fillText('Negative', margin + cellW / 2, gridH + 20);
108
+ ctx.fillText('Positive', margin + cellW + cellW / 2, gridH + 20);
109
+ ctx.save(); ctx.translate(20, gridH / 2); ctx.rotate(-Math.PI / 2);
110
+ ctx.fillText('Positive', 0, 0); ctx.fillText('Negative', -cellH, 0);
111
+ ctx.restore();
112
+ }
113
+
114
+ // --- UI UPDATE ---
115
+ function updateApplication() {
116
+ const separation = parseFloat(document.getElementById('separationSlider').value);
117
+ const stdDev = parseFloat(document.getElementById('stdDevSlider').value);
118
+ document.getElementById('separationValue').textContent = separation.toFixed(1);
119
+ document.getElementById('stdDevValue').textContent = stdDev.toFixed(1);
120
+
121
+ const { data, labels } = generateData(separation, stdDev);
122
+ const model = new GaussianNB();
123
+ model.fit(data, labels);
124
+ const scores = model.predict_proba(data);
125
+ const { rocPoints, auc } = calculateRocAndAuc(labels, scores);
126
+ const { vp, fp, vn, fn } = getConfusionMatrix(labels, scores, 0.5);
127
+ const total = vp + fp + vn + fn;
128
+ const precision = (vp + fp) > 0 ? vp / (vp + fp) : 0;
129
+ const recall = (vp + fn) > 0 ? vp / (vp + fn) : 0;
130
+ const specificity = (vn + fp) > 0 ? vn / (vn + fp) : 0;
131
+ const f1score = (precision + recall) > 0 ? 2 * (precision * recall) / (precision + recall) : 0;
132
+ const accuracy = total > 0 ? (vp + vn) / total : 0;
133
+
134
+ drawConfusionMatrix('matrixChart', vp, fp, vn, fn);
135
+
136
+ dataChart.data.datasets[0].data = data.filter((_, i) => labels[i] === 0);
137
+ dataChart.data.datasets[1].data = data.filter((_, i) => labels[i] === 1);
138
+ dataChart.update('none');
139
+
140
+ rocChart.data.datasets[0].data = rocPoints;
141
+ rocChart.update('none');
142
+
143
+ metricsChart.data.datasets[0].data = [auc, accuracy, precision, recall, specificity, f1score];
144
+ metricsChart.update('none');
145
+ }
146
+
147
+ // --- INITIALIZATION ---
148
+ const customDatalabelsPlugin = {
149
+ id: 'customDatalabels',
150
+ afterDatasetsDraw: (chart) => {
151
+ const ctx = chart.ctx;
152
+ ctx.save();
153
+ ctx.font = 'bold 12px Segoe UI';
154
+ ctx.fillStyle = 'white';
155
+ ctx.textAlign = 'center';
156
+ chart.data.datasets.forEach((dataset, i) => {
157
+ const meta = chart.getDatasetMeta(i);
158
+ meta.data.forEach((bar, index) => {
159
+ const data = dataset.data[index];
160
+ if (bar.height > 15) {
161
+ ctx.textBaseline = 'bottom';
162
+ ctx.fillText(data.toFixed(3), bar.x, bar.y + bar.height - 5);
163
+ }
164
+ });
165
+ });
166
+ ctx.restore();
167
+ }
168
+ };
169
+
170
+ function initCharts() {
171
+ const dataCtx = document.getElementById('dataChart').getContext('2d');
172
+ 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 } } });
173
+ const rocCtx = document.getElementById('rocChart').getContext('2d');
174
+ rocChart = new Chart(rocCtx, { type: 'scatter', data: { datasets: [{ label: 'ROC Curve', data: [], borderColor: '#0D47A1', backgroundColor: 'transparent', showLine: true, pointRadius: 0, borderWidth: 3 }, { label: 'Chance Line', data: [{ x: 0, y: 0 }, { x: 1, y: 1 }], borderColor: '#666', showLine: true, pointRadius: 0, borderDash: [5, 5] }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0 }, scales: { x: { min: 0, max: 1, title: { display: true, text: 'False Positive Rate' } }, y: { min: 0, max: 1, title: { display: true, text: 'True Positive Rate' } } } } });
175
+ const metricsCtx = document.getElementById('metricsChart').getContext('2d');
176
+ metricsChart = new Chart(metricsCtx, {
177
+ type: 'bar',
178
+ data: { labels: ['AUC', 'Accuracy', 'Precision', 'Recall', 'Specificity', 'F1-Score'], datasets: [{ data: [], backgroundColor: ['#673AB7', '#009688', '#1E88E5', '#388E3C', '#FB8C00', '#9C27B0'] }] },
179
+ plugins: [customDatalabelsPlugin],
180
+ options: { responsive: true, maintainAspectRatio: false, indexAxis: 'x', animation: { duration: 0 }, plugins: { legend: { display: false }, tooltip: { enabled: false } }, scales: { y: { beginAtZero: true, max: 1 } } }
181
+ });
182
+ }
183
+
184
+ function makeDraggable(element, handle) {
185
+ let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
186
+ handle.onmousedown = (e) => { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; };
187
+ 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"; };
188
+ const closeDragElement = () => { document.onmouseup = null; document.onmousemove = null; };
189
+ }
190
+
191
+ window.addEventListener('load', function () {
192
+ initCharts();
193
+ const sliders = ['separationSlider', 'stdDevSlider'];
194
+ sliders.forEach(id => { document.getElementById(id).addEventListener('input', updateApplication); });
195
+ if (window.innerWidth > 1200) { makeDraggable(document.getElementById('floatingControls'), document.getElementById('controlsTitle')); }
196
+ updateApplication();
197
+ });
src/js/inverse_classifier.js ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // --- GLOBAL VARIABLES ---
2
+ let scoresChart, rocChart, metricsChart;
3
+ let lockState = { tp: false, fp: false, tn: false, fn: false };
4
+ let currentState = { tp: 70, fp: 20, tn: 80, fn: 30 };
5
+ const TOTAL_SAMPLES = 200;
6
+
7
+ // --- SIMULATION & CALCULATIONS ---
8
+ function randomGaussian(mean = 0, stdDev = 1) {
9
+ let u = 0, v = 0;
10
+ while (u === 0) u = Math.random();
11
+ while (v === 0) v = Math.random();
12
+ return mean + stdDev * Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
13
+ }
14
+
15
+ function generateScoresFromMatrix(tp, fp, tn, fn) {
16
+ const total_pos_real = tp + fn;
17
+ const total_neg_real = tn + fp;
18
+ if (total_pos_real === 0 || total_neg_real === 0) return { scores: [], labels: [] };
19
+ const tpr = tp / total_pos_real;
20
+ const tnr = tn / total_neg_real;
21
+ const mean_pos = 0.5 + (tpr - 0.5);
22
+ const mean_neg = 0.5 - (tnr - 0.5);
23
+ const stdDev = 0.15;
24
+ const scores = [], labels = [];
25
+ for (let i = 0; i < total_neg_real; i++) { scores.push(randomGaussian(mean_neg, stdDev)); labels.push(0); }
26
+ for (let i = 0; i < total_pos_real; i++) { scores.push(randomGaussian(mean_pos, stdDev)); labels.push(1); }
27
+ return { scores, labels };
28
+ }
29
+
30
+ function calculateRocAndAuc(labels, scores) {
31
+ const pairs = labels.map((label, i) => ({ label, score: scores[i] }));
32
+ pairs.sort((a, b) => b.score - a.score);
33
+ let tp = 0, fp = 0;
34
+ const total_pos = labels.filter(l => l === 1).length;
35
+ const total_neg = labels.length - total_pos;
36
+ if (total_pos === 0 || total_neg === 0) return { rocPoints: [{ x: 0, y: 0 }, { x: 1, y: 1 }], auc: 0.5 };
37
+ const rocPoints = [{ x: 0, y: 0 }];
38
+ let auc = 0, prev_tpr = 0, prev_fpr = 0;
39
+ for (const pair of pairs) {
40
+ if (pair.label === 1) tp++; else fp++;
41
+ const tpr = tp / total_pos;
42
+ const fpr = fp / total_neg;
43
+ auc += (tpr + prev_tpr) / 2 * (fpr - prev_fpr);
44
+ rocPoints.push({ x: fpr, y: tpr });
45
+ prev_tpr = tpr; prev_fpr = fpr;
46
+ }
47
+ return { rocPoints, auc };
48
+ }
49
+
50
+ function createHistogramData(scores, labels, n_bins = 20) {
51
+ const bins = Array(n_bins).fill(0).map(() => ({ pos: 0, neg: 0 }));
52
+ const bin_labels = Array(n_bins).fill(0).map((_, i) => (i / n_bins).toFixed(2));
53
+ scores.forEach((score, i) => {
54
+ let bin_index = Math.floor(score * n_bins);
55
+ if (bin_index < 0) bin_index = 0;
56
+ if (bin_index >= n_bins) bin_index = n_bins - 1;
57
+ if (labels[i] === 1) bins[bin_index].pos++;
58
+ else bins[bin_index].neg++;
59
+ });
60
+ return { labels: bin_labels, pos_data: bins.map(b => b.pos), neg_data: bins.map(b => b.neg) };
61
+ }
62
+
63
+ // [CORRIGÉ] Fonction de dessin utilisant la palette "Blues"
64
+ function drawConfusionMatrix(canvasId, tp, fp, tn, fn) {
65
+ const canvas = document.getElementById(canvasId);
66
+ const ctx = canvas.getContext('2d');
67
+ const w = canvas.width, h = canvas.height;
68
+ ctx.clearRect(0, 0, w, h);
69
+ const margin = 50, gridW = w - margin, gridH = h - margin, cellW = gridW / 2, cellH = gridH / 2;
70
+ const max_val = Math.max(tp, fp, tn, fn);
71
+ const baseColor = [8, 48, 107]; // "Blues" palette base color
72
+ const cells = [
73
+ { label: 'TN', value: tn, x: 0, y: cellH },
74
+ { label: 'FP', value: fp, x: cellW, y: cellH },
75
+ { label: 'FN', value: fn, x: 0, y: 0 },
76
+ { label: 'TP', value: tp, x: cellW, y: 0 }
77
+ ];
78
+ cells.forEach(cell => {
79
+ const intensity = max_val > 0 ? cell.value / max_val : 0;
80
+ ctx.fillStyle = `rgba(${baseColor[0]}, ${baseColor[1]}, ${baseColor[2]}, ${intensity})`;
81
+ ctx.fillRect(margin + cell.x, cell.y, cellW, cellH);
82
+ ctx.fillStyle = intensity > 0.5 ? 'white' : 'black';
83
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
84
+ ctx.font = 'bold 20px Segoe UI'; ctx.fillText(cell.label, margin + cell.x + cellW / 2, cell.y + cellH / 2 - 12);
85
+ ctx.font = '18px Segoe UI'; ctx.fillText(cell.value, margin + cell.x + cellW / 2, cell.y + cellH / 2 + 12);
86
+ });
87
+ ctx.fillStyle = '#333'; ctx.font = 'bold 14px Segoe UI';
88
+ ctx.fillText('Negative', margin + cellW / 2, gridH + 20);
89
+ ctx.fillText('Positive', margin + cellW + cellW / 2, gridH + 20);
90
+ ctx.save(); ctx.translate(20, gridH / 2); ctx.rotate(-Math.PI / 2);
91
+ ctx.fillText('Positive', 0, 0); ctx.fillText('Negative', -cellH, 0);
92
+ ctx.restore();
93
+ }
94
+
95
+ // --- UI MANAGEMENT ---
96
+ function adjustValues(changedParam, newValue) {
97
+ let values = { ...currentState };
98
+ values[changedParam] = newValue;
99
+ let diff = Object.values(values).reduce((sum, v) => sum + v, 0) - TOTAL_SAMPLES;
100
+ const unlockedParams = Object.keys(values).filter(p => p !== changedParam && !lockState[p]);
101
+ while (diff !== 0 && unlockedParams.length > 0) {
102
+ let adjustedInLoop = false;
103
+ if (diff > 0) {
104
+ unlockedParams.sort((a, b) => values[b] - values[a]);
105
+ for (const param of unlockedParams) { if (values[param] > 0) { values[param]--; diff--; adjustedInLoop = true; break; } }
106
+ } else {
107
+ unlockedParams.sort((a, b) => values[a] - values[b]);
108
+ for (const param of unlockedParams) { if (values[param] < TOTAL_SAMPLES) { values[param]++; diff++; adjustedInLoop = true; break; } }
109
+ }
110
+ if (!adjustedInLoop) break;
111
+ }
112
+ if (diff === 0) { currentState = { ...values }; }
113
+ updateUI();
114
+ }
115
+
116
+ function updateUI() {
117
+ const { tp, fp, tn, fn } = currentState;
118
+ const total = tp + fp + tn + fn;
119
+ const precision = (tp + fp) > 0 ? tp / (tp + fp) : 0;
120
+ const recall = (tp + fn) > 0 ? tp / (tp + fn) : 0;
121
+ const specificity = (tn + fp) > 0 ? tn / (tn + fp) : 0;
122
+ const f1score = (precision + recall) > 0 ? 2 * (precision * recall) / (precision + recall) : 0;
123
+ const accuracy = total > 0 ? (tp + tn) / total : 0;
124
+ const { scores, labels } = generateScoresFromMatrix(tp, fp, tn, fn);
125
+ const { rocPoints, auc } = calculateRocAndAuc(labels, scores);
126
+ const histogram = createHistogramData(scores, labels);
127
+
128
+ for (const param in currentState) {
129
+ document.getElementById(`${param}Slider`).value = currentState[param];
130
+ document.getElementById(`${param}Value`).textContent = currentState[param];
131
+ }
132
+
133
+ drawConfusionMatrix('matrixChart', tp, fp, tn, fn);
134
+
135
+ scoresChart.data.labels = histogram.labels;
136
+ scoresChart.data.datasets[0].data = histogram.neg_data;
137
+ scoresChart.data.datasets[1].data = histogram.pos_data;
138
+ scoresChart.update('none');
139
+
140
+ rocChart.data.datasets[0].data = rocPoints;
141
+ rocChart.update('none');
142
+
143
+ metricsChart.data.datasets[0].data = [auc, accuracy, precision, recall, specificity, f1score];
144
+ metricsChart.update('none');
145
+ }
146
+
147
+ // --- INITIALIZATION ---
148
+ const customDatalabelsPlugin = {
149
+ id: 'customDatalabels',
150
+ afterDatasetsDraw: (chart) => {
151
+ const ctx = chart.ctx;
152
+ ctx.save();
153
+ ctx.font = 'bold 12px Segoe UI';
154
+ ctx.fillStyle = 'white';
155
+ ctx.textAlign = 'center';
156
+ chart.data.datasets.forEach((dataset, i) => {
157
+ const meta = chart.getDatasetMeta(i);
158
+ meta.data.forEach((bar, index) => {
159
+ const data = dataset.data[index];
160
+ if (bar.height > 15) {
161
+ ctx.textBaseline = 'bottom';
162
+ ctx.fillText(data.toFixed(3), bar.x, bar.y + bar.height - 5);
163
+ }
164
+ });
165
+ });
166
+ ctx.restore();
167
+ }
168
+ };
169
+
170
+ function initCharts() {
171
+ const scoresCtx = document.getElementById('scoresChart').getContext('2d');
172
+ 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' } } } } });
173
+ const rocCtx = document.getElementById('rocChart').getContext('2d');
174
+ rocChart = new Chart(rocCtx, { type: 'scatter', data: { datasets: [{ label: 'ROC Curve', data: [], borderColor: '#0D47A1', backgroundColor: 'transparent', showLine: true, pointRadius: 0, borderWidth: 3 }, { label: 'Chance Line', data: [{ x: 0, y: 0 }, { x: 1, y: 1 }], borderColor: '#666', showLine: true, pointRadius: 0, borderDash: [5, 5] }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0 }, scales: { x: { min: 0, max: 1, title: { display: true, text: 'False Positive Rate' } }, y: { min: 0, max: 1, title: { display: true, text: 'True Positive Rate' } } } } });
175
+
176
+ const metricsCtx = document.getElementById('metricsChart').getContext('2d');
177
+ metricsChart = new Chart(metricsCtx, {
178
+ type: 'bar',
179
+ data: { labels: ['AUC', 'Accuracy', 'Precision', 'Recall', 'Specificity', 'F1-Score'], datasets: [{ data: [], backgroundColor: ['#673AB7', '#009688', '#1E88E5', '#388E3C', '#FB8C00', '#9C27B0'] }] },
180
+ plugins: [customDatalabelsPlugin],
181
+ options: { responsive: true, maintainAspectRatio: false, indexAxis: 'x', animation: { duration: 0 }, plugins: { legend: { display: false }, tooltip: { enabled: false } }, scales: { y: { beginAtZero: true, max: 1 } } }
182
+ });
183
+ }
184
+
185
+ function updateSliderDisabledState() {
186
+ const sliders = { tp: document.getElementById('tpSlider'), fp: document.getElementById('fpSlider'), tn: document.getElementById('tnSlider'), fn: document.getElementById('fnSlider') };
187
+ const lockedCount = Object.values(lockState).filter(isLocked => isLocked).length;
188
+ for (const param in lockState) { sliders[param].disabled = lockState[param] || lockedCount >= 3; }
189
+ }
190
+
191
+ function makeDraggable(element, handle) {
192
+ let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
193
+ handle.onmousedown = (e) => { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; };
194
+ 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"; };
195
+ const closeDragElement = () => { document.onmouseup = null; document.onmousemove = null; };
196
+ }
197
+
198
+ window.addEventListener('load', function () {
199
+ initCharts();
200
+ Object.keys(currentState).forEach(param => {
201
+ const slider = document.getElementById(`${param}Slider`);
202
+ slider.addEventListener('input', () => document.getElementById(`${param}Value`).textContent = slider.value);
203
+ slider.addEventListener('change', () => adjustValues(param, parseInt(slider.value)));
204
+ });
205
+ document.querySelectorAll('.lock-toggle').forEach(lock => {
206
+ lock.addEventListener('click', function () {
207
+ const param = this.dataset.param;
208
+ lockState[param] = !lockState[param];
209
+ this.textContent = lockState[param] ? '🔒' : '🔓';
210
+ this.classList.toggle('locked', lockState[param]);
211
+ updateSliderDisabledState();
212
+ });
213
+ });
214
+ if (window.innerWidth > 1200) { makeDraggable(document.getElementById('floatingControls'), document.getElementById('controlsTitle')); }
215
+ updateSliderDisabledState();
216
+ updateUI();
217
+ });