Commit
·
41cacc0
1
Parent(s):
5d64457
Refactor: Reorganize project structure and add README
Browse files- README.md +59 -2
- direct_classifier.html +67 -0
- inverse_classifier.html +81 -0
- src/assets/logo.jpg +3 -0
- src/css/inverse_style.css +230 -0
- src/css/style.css +170 -0
- src/js/direct_classifier.js +197 -0
- src/js/inverse_classifier.js +217 -0
README.md
CHANGED
|
@@ -1,2 +1,59 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# School of Statistics - Interactive Classification Dashboards
|
| 2 |
+
|
| 3 |
+

|
| 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
|
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 |
+
});
|