diff --git a/app_config.py b/app_config.py index d7423bc..a9ff8c6 100644 --- a/app_config.py +++ b/app_config.py @@ -366,8 +366,41 @@ class ConfigManager: return self.default_config['grading_system']['types'] def get_special_values(self) -> Dict[str, Dict[str, Any]]: - """Récupère les valeurs spéciales configurables (., d, a).""" - return self.default_config['grading_system']['special_values'] + """Récupère les valeurs spéciales configurables (., d, a) depuis la base de données.""" + # Récupérer les valeurs spéciales depuis la base de données + special_values_keys = ['.', 'd', 'a'] + scale_values = CompetenceScaleValue.query.filter( + CompetenceScaleValue.value.in_(special_values_keys) + ).all() + + # Si aucune valeur n'existe, initialiser avec les valeurs par défaut + if not scale_values: + self.initialize_default_config() + scale_values = CompetenceScaleValue.query.filter( + CompetenceScaleValue.value.in_(special_values_keys) + ).all() + + # Construire le dict avec mapping included_in_total -> counts + result = {} + for scale_value in scale_values: + # Récupérer les valeurs par défaut pour compléter les informations manquantes + default_config = self.default_config['grading_system']['special_values'].get(scale_value.value, {}) + + result[scale_value.value] = { + 'label': scale_value.label, + 'color': scale_value.color, + 'counts': scale_value.included_in_total, # Mapping: included_in_total -> counts + 'description': default_config.get('description', scale_value.label), + 'value': default_config.get('value', 0 if scale_value.included_in_total else None) + } + + # Ajouter les valeurs manquantes depuis la config par défaut si elles n'existent pas en base + default_special = self.default_config['grading_system']['special_values'] + for key, default_value in default_special.items(): + if key not in result: + result[key] = default_value + + return result def get_score_meanings(self) -> Dict[int, Dict[str, str]]: """Récupère les significations des scores (0-3) depuis la base de données.""" diff --git a/routes/assessments.py b/routes/assessments.py index 5671e9b..2e8ec7a 100644 --- a/routes/assessments.py +++ b/routes/assessments.py @@ -193,10 +193,86 @@ def results(id): # Calculer les scores des élèves students_scores, exercise_scores = assessment.calculate_student_scores() - + # Trier les élèves par ordre alphabétique - sorted_students = sorted(students_scores.values(), + sorted_students = sorted(students_scores.values(), key=lambda x: (x['student'].last_name.lower(), x['student'].first_name.lower())) + + # Enrichir les données avec les valeurs spéciales pour l'affichage + from app_config import config_manager + scale_colors = config_manager.get_competence_scale_values() + + for student_data in sorted_students: + student = student_data['student'] + # Analyser les valeurs spéciales pour chaque exercice + for exercise in assessment.exercises: + if exercise.id in student_data['exercises']: + exercise_data = student_data['exercises'][exercise.id] + + # Collecter toutes les valeurs pour cet exercice + all_values = [] + has_counted_values = False + + for element in exercise.grading_elements: + grade = None + for g in element.grades: + if g.student_id == student.id: + grade = g + break + + if grade and grade.value: + all_values.append(grade.value) + # Vérifier si cette valeur compte dans le calcul + from models import GradingCalculator + if GradingCalculator.is_counted_in_total(grade.value, element.grading_type): + has_counted_values = True + + # Si toutes les valeurs sont spéciales et identiques ET qu'aucune ne compte dans le total + if all_values and not has_counted_values: + unique_values = set(all_values) + if len(unique_values) == 1: + unique_value = next(iter(unique_values)) + if unique_value in scale_colors: + exercise_data['special_display'] = unique_value + else: + exercise_data['special_display'] = None + else: + exercise_data['special_display'] = None + else: + exercise_data['special_display'] = None + + # Analyser les valeurs spéciales pour le total global + all_student_values = [] + has_any_counted_values = False + + for exercise in assessment.exercises: + for element in exercise.grading_elements: + grade = None + for g in element.grades: + if g.student_id == student.id: + grade = g + break + + if grade and grade.value: + all_student_values.append(grade.value) + # Vérifier si cette valeur compte dans le calcul + from models import GradingCalculator + if GradingCalculator.is_counted_in_total(grade.value, element.grading_type): + has_any_counted_values = True + + # Si toutes les valeurs sont spéciales et identiques ET qu'aucune ne compte dans le total + if all_student_values and not has_any_counted_values: + unique_values = set(all_student_values) + if len(unique_values) == 1: + unique_value = next(iter(unique_values)) + if unique_value in scale_colors: + student_data['total_special_display'] = unique_value + else: + student_data['total_special_display'] = None + else: + student_data['total_special_display'] = None + else: + student_data['total_special_display'] = None # Calculer les statistiques statistics = assessment.get_assessment_statistics() @@ -226,16 +302,19 @@ def results(id): students_list = [f"{s['student'].last_name} {s['student'].first_name}" for s in sorted_students] competences_list = sorted(competences_in_eval) - # Calculer les scores par élève/compétence + # Calculer les scores par élève/compétence avec détection des valeurs spéciales competences_scores_matrix = [] + competences_special_values_matrix = [] # Nouvelle matrice pour les valeurs spéciales + for student_data in sorted_students: student_scores_by_competence = {} student_totals_by_competence = {} - + student_special_values_by_competence = {} # Stocker les valeurs spéciales + for exercise_id, exercise_data in student_data['exercises'].items(): # Récupérer l'exercice pour accéder aux grading_elements exercise = next(ex for ex in assessment.exercises if ex.id == exercise_id) - + for element in exercise.grading_elements: if element.skill and element.skill in competences_in_eval: # Trouver la note correspondante @@ -244,9 +323,20 @@ def results(id): if g.student_id == student_data['student'].id: grade = g break - + if grade and grade.value: from models import GradingCalculator + + # Vérifier d'abord si c'est une valeur spéciale + from app_config import config_manager + scale_colors = config_manager.get_competence_scale_values() + + if grade.value in scale_colors: + # C'est une valeur spéciale + if element.skill not in student_special_values_by_competence: + student_special_values_by_competence[element.skill] = [] + student_special_values_by_competence[element.skill].append(grade.value) + score = GradingCalculator.calculate_score(grade.value, element.grading_type, element.max_points) if score is not None: # Exclure les dispensés if element.skill not in student_scores_by_competence: @@ -254,17 +344,30 @@ def results(id): student_totals_by_competence[element.skill] = 0 student_scores_by_competence[element.skill] += score student_totals_by_competence[element.skill] += element.max_points - + # Calculer les pourcentages par compétence pour cet élève student_row = [] + student_special_row = [] for comp in competences_list: if comp in student_scores_by_competence and student_totals_by_competence[comp] > 0: percentage = (student_scores_by_competence[comp] / student_totals_by_competence[comp]) * 100 student_row.append(round(percentage, 1)) + + # Si toutes les notes sont des valeurs spéciales et identiques, utiliser cette valeur + if comp in student_special_values_by_competence: + special_values = student_special_values_by_competence[comp] + if len(set(special_values)) == 1: # Toutes les valeurs sont identiques + student_special_row.append(special_values[0]) + else: + student_special_row.append(None) + else: + student_special_row.append(None) else: student_row.append(None) # Pas de données pour cette compétence - + student_special_row.append(None) + competences_scores_matrix.append(student_row) + competences_special_values_matrix.append(student_special_row) # Préparer les données heatmap domaines domains_list = [] @@ -277,29 +380,43 @@ def results(id): domains_list.append(domain_name) domains_colors[domain_name] = all_domains[domain_id]['color'] - # Calculer les scores par élève/domaine + # Calculer les scores par élève/domaine avec détection des valeurs spéciales domains_scores_matrix = [] + domains_special_values_matrix = [] # Nouvelle matrice pour les valeurs spéciales des domaines + for student_data in sorted_students: student_scores_by_domain = {} student_totals_by_domain = {} - + student_special_values_by_domain = {} # Stocker les valeurs spéciales + for exercise_id, exercise_data in student_data['exercises'].items(): # Récupérer l'exercice pour accéder aux grading_elements exercise = next(ex for ex in assessment.exercises if ex.id == exercise_id) - + for element in exercise.grading_elements: if element.domain_id and element.domain_id in domains_in_eval: domain_name = all_domains[element.domain_id]['name'] - + # Trouver la note correspondante grade = None for g in element.grades: if g.student_id == student_data['student'].id: grade = g break - + if grade and grade.value: from models import GradingCalculator + + # Vérifier d'abord si c'est une valeur spéciale + from app_config import config_manager + scale_colors = config_manager.get_competence_scale_values() + + if grade.value in scale_colors: + # C'est une valeur spéciale + if domain_name not in student_special_values_by_domain: + student_special_values_by_domain[domain_name] = [] + student_special_values_by_domain[domain_name].append(grade.value) + score = GradingCalculator.calculate_score(grade.value, element.grading_type, element.max_points) if score is not None: # Exclure les dispensés if domain_name not in student_scores_by_domain: @@ -307,17 +424,30 @@ def results(id): student_totals_by_domain[domain_name] = 0 student_scores_by_domain[domain_name] += score student_totals_by_domain[domain_name] += element.max_points - + # Calculer les pourcentages par domaine pour cet élève student_row = [] + student_special_row = [] for domain in domains_list: if domain in student_scores_by_domain and student_totals_by_domain[domain] > 0: percentage = (student_scores_by_domain[domain] / student_totals_by_domain[domain]) * 100 student_row.append(round(percentage, 1)) + + # Si toutes les notes sont des valeurs spéciales et identiques, utiliser cette valeur + if domain in student_special_values_by_domain: + special_values = student_special_values_by_domain[domain] + if len(set(special_values)) == 1: # Toutes les valeurs sont identiques + student_special_row.append(special_values[0]) + else: + student_special_row.append(None) + else: + student_special_row.append(None) else: student_row.append(None) # Pas de données pour ce domaine - + student_special_row.append(None) + domains_scores_matrix.append(student_row) + domains_special_values_matrix.append(student_special_row) # Préparer les couleurs des compétences pour celles présentes dans l'évaluation competences_colors = {comp: all_competences.get(comp, '#6b7280') for comp in competences_list} @@ -326,13 +456,15 @@ def results(id): 'students': students_list, 'competences': competences_list, 'scores': competences_scores_matrix, + 'special_values': competences_special_values_matrix, 'colors': competences_colors } if competences_list else None - + heatmap_domains = { 'students': students_list, 'domains': domains_list, 'scores': domains_scores_matrix, + 'special_values': domains_special_values_matrix, 'colors': domains_colors } if domains_list else None diff --git a/templates/assessment_results.html b/templates/assessment_results.html index c021c45..d1b1165 100644 --- a/templates/assessment_results.html +++ b/templates/assessment_results.html @@ -73,46 +73,255 @@ - + {% if heatmap_competences %}
Heatmap des scores moyens de chaque élève par compétence
+Scores moyens de chaque élève par compétence (%) - Cliquez sur les en-têtes pour trier
| + Élève + ⇅ + | + {% for competence in heatmap_competences.competences %} ++ {{ competence }} + ⇅ + | + {% endfor %} +
|---|---|
| + {{ heatmap_competences.students[student_index] }} + | + {% for competence_index in range(heatmap_competences.competences|length) %} + {% set score = heatmap_competences.scores[student_index][competence_index] %} + {% set special_value = heatmap_competences.special_values[student_index][competence_index] %} ++ {% if special_value and special_value in heatmap_grading_elements.scale_colors %} + {{ special_value }} + {% elif score is not none %} + {{ "%.1f"|format(score) }}% + {% else %} + - + {% endif %} + | + {% endfor %} +
Heatmap des scores moyens de chaque élève par domaine
+Scores moyens de chaque élève par domaine (%) - Cliquez sur les en-têtes pour trier
| + Élève + ⇅ + | + {% for domain in heatmap_domains.domains %} ++ {{ domain }} + ⇅ + | + {% endfor %} +
|---|---|
| + {{ heatmap_domains.students[student_index] }} + | + {% for domain_index in range(heatmap_domains.domains|length) %} + {% set score = heatmap_domains.scores[student_index][domain_index] %} + {% set special_value = heatmap_domains.special_values[student_index][domain_index] %} ++ {% if special_value and special_value in heatmap_grading_elements.scale_colors %} + {{ special_value }} + {% elif score is not none %} + {{ "%.1f"|format(score) }}% + {% else %} + - + {% endif %} + | + {% endfor %} +
Heatmap détaillée des scores de chaque élève sur chaque question/compétence
+Détail des scores de chaque élève sur chaque question/compétence - Cliquez sur les en-têtes pour trier
| + Élève + ⇅ + | + {% for element in heatmap_grading_elements.elements %} ++ {{ element|truncate(12, True) }} + ⇅ + | + {% endfor %} +
|---|---|
| + {{ heatmap_grading_elements.students[student_index] }} + | + {% for element_index in range(heatmap_grading_elements.elements|length) %} + {% set grade_data = heatmap_grading_elements.detailed_scores[student_index][element_index] %} ++ {% if grade_data %} + {{ grade_data.value }} + {% else %} + - + {% endif %} + | + {% endfor %} +
Cliquez sur les en-têtes pour trier
| + | Élève + ⇅ | -+ | Note totale + ⇅ | {% for exercise in assessment.exercises|sort(attribute='order') %} -+ | {{ exercise.title }} + ⇅ | {% endfor %}
+ {% if student_data.total_special_display %}
+
+ {{ student_data.total_special_display }}
+
+ {% else %}
{{ "%.1f"|format(student_data.total_score) }} / {{ "%.1f"|format(student_data.total_max_points) }}
+ {% endif %}
|
{% for exercise in assessment.exercises|sort(attribute='order') %}
-
- {% if exercise.id in student_data.exercises %}
- {{ "%.1f"|format(student_data.exercises[exercise.id].score) }} / {{ "%.1f"|format(student_data.exercises[exercise.id].max_points) }}
+ {% if exercise.id in student_data.exercises %}
+ {% set exercise_data = student_data.exercises[exercise.id] %}
+ {% if exercise_data.special_display %}
+
+ {% else %}
+
+ {{ exercise_data.special_display }}
+
{% else %}
- -
+
+ {{ "%.1f"|format(exercise_data.score) }} / {{ "%.1f"|format(exercise_data.max_points) }}
+
{% endif %}
- -
+ {% endif %}
|
{% endfor %}
@@ -290,490 +520,6 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
});
-
- // === HEATMAPS ===
-
- // === HEATMAPS DATA ===
- {% if heatmap_competences %}
- const competencesData = {{ heatmap_competences | tojson }};
- {% endif %}
-
- {% if heatmap_domains %}
- const domainsData = {{ heatmap_domains | tojson }};
- {% endif %}
-
- {% if heatmap_grading_elements %}
- const gradingElementsData = {{ heatmap_grading_elements | tojson }};
- {% endif %}
-
- // Fonctions utilitaires pour l'interpolation HSL (globales pour être réutilisables)
- function hexToRgb(hex) {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
- return result ? {
- r: parseInt(result[1], 16),
- g: parseInt(result[2], 16),
- b: parseInt(result[3], 16)
- } : null;
- }
-
- function rgbToHex(r, g, b) {
- return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
- }
-
- function rgbToHsl(r, g, b) {
- r /= 255;
- g /= 255;
- b /= 255;
-
- const max = Math.max(r, g, b);
- const min = Math.min(r, g, b);
- let h, s, l = (max + min) / 2;
-
- if (max === min) {
- h = s = 0; // achromatic
- } else {
- const d = max - min;
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
-
- switch (max) {
- case r: h = (g - b) / d + (g < b ? 6 : 0); break;
- case g: h = (b - r) / d + 2; break;
- case b: h = (r - g) / d + 4; break;
- }
- h /= 6;
- }
-
- return { h: h * 360, s: s * 100, l: l * 100 };
- }
-
- function hslToRgb(h, s, l) {
- h /= 360;
- s /= 100;
- l /= 100;
-
- const hue2rgb = (p, q, t) => {
- if (t < 0) t += 1;
- if (t > 1) t -= 1;
- if (t < 1/6) return p + (q - p) * 6 * t;
- if (t < 1/2) return q;
- if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
- return p;
- };
-
- let r, g, b;
-
- if (s === 0) {
- r = g = b = l; // achromatic
- } else {
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
- const p = 2 * l - q;
- r = hue2rgb(p, q, h + 1/3);
- g = hue2rgb(p, q, h);
- b = hue2rgb(p, q, h - 1/3);
- }
-
- return {
- r: Math.round(r * 255),
- g: Math.round(g * 255),
- b: Math.round(b * 255)
- };
- }
-
- function interpolateColorHSL(color1, color2, factor) {
- const rgb1 = hexToRgb(color1);
- const rgb2 = hexToRgb(color2);
-
- if (!rgb1 || !rgb2) return color1;
-
- const hsl1 = rgbToHsl(rgb1.r, rgb1.g, rgb1.b);
- const hsl2 = rgbToHsl(rgb2.r, rgb2.g, rgb2.b);
-
- // Interpolation en HSL
- let h1 = hsl1.h;
- let h2 = hsl2.h;
-
- // Gérer la transition des teintes (éviter le long chemin autour du cercle)
- let deltaH = h2 - h1;
- if (deltaH > 180) {
- h2 -= 360;
- } else if (deltaH < -180) {
- h2 += 360;
- }
-
- const h = h1 + (h2 - h1) * factor;
- const s = hsl1.s + (hsl2.s - hsl1.s) * factor;
- const l = hsl1.l + (hsl2.l - hsl1.l) * factor;
-
- // Normaliser la teinte
- const normalizedH = ((h % 360) + 360) % 360;
-
- const rgb = hslToRgb(normalizedH, s, l);
- return rgbToHex(rgb.r, rgb.g, rgb.b);
- }
-
- function createHeatmap(canvasId, data, type) {
- const canvas = document.getElementById(canvasId);
- const ctx = canvas.getContext('2d');
-
- const students = data.students;
- const categories = type === 'compétence' ? data.competences :
- (type === 'domaine' ? data.domains : data.elements);
- const scores = data.scores;
- const colors = data.colors;
-
- if (!categories || categories.length === 0) return;
-
- // Dimensions adaptées selon le type
- const padding = type === 'élément' ? 120 : 80; // Plus d'espace pour les libellés longs
- const minCellWidth = type === 'élément' ? 50 : 60;
- const minCellHeight = type === 'élément' ? 30 : 40; // Plus compact pour les éléments
-
- const cellWidth = Math.max(minCellWidth, (canvas.width - padding * 2) / students.length);
- const cellHeight = Math.max(minCellHeight, (canvas.height - padding * 2) / categories.length);
-
- // Ajuster la taille du canvas si nécessaire
- canvas.width = cellWidth * students.length + padding * 2;
- canvas.height = cellHeight * categories.length + padding * 2;
-
- // Effacer le canvas
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- ctx.font = '12px Arial';
-
- // Fonction pour convertir un score en couleur avec dégradé continu HSL
- function getScoreColor(score) {
- if (score === null || score === undefined) {
- return '#f3f4f6'; // Gris clair pour les données manquantes
- }
-
- // Dégradé continu du rouge (0%) au vert (100%) en utilisant l'interpolation HSL
- const percentage = Math.max(0, Math.min(100, score));
- const factor = percentage / 100;
-
- // Couleurs de début et fin (rouge vers vert)
- const redColor = '#ef4444'; // Rouge (même que dans la config)
- const greenColor = '#22c55e'; // Vert (même que dans la config)
-
- return interpolateColorHSL(redColor, greenColor, factor);
- }
-
- // Dessiner les cellules
- for (let studentIndex = 0; studentIndex < students.length; studentIndex++) {
- for (let categoryIndex = 0; categoryIndex < categories.length; categoryIndex++) {
- const score = scores[studentIndex][categoryIndex];
- const x = padding + studentIndex * cellWidth;
- const y = padding + categoryIndex * cellHeight;
-
- // Couleur de fond basée sur le score
- ctx.fillStyle = getScoreColor(score);
- ctx.fillRect(x, y, cellWidth - 1, cellHeight - 1);
-
- // Bordure
- ctx.strokeStyle = '#d1d5db';
- ctx.strokeRect(x, y, cellWidth - 1, cellHeight - 1);
-
- // Texte du score
- ctx.font = type === 'élément' ? '10px Arial' : '12px Arial'; // Police adaptée
- if (score !== null && score !== undefined) {
- ctx.fillStyle = score > 60 ? '#ffffff' : '#000000'; // Blanc sur foncé, noir sur clair
- ctx.textAlign = 'center';
- // Affichage simplifié pour les éléments (sans %)
- const displayText = type === 'élément' ? score.toFixed(0) : score.toFixed(1) + '%';
- ctx.fillText(displayText, x + cellWidth/2, y + cellHeight/2 + 4);
- } else {
- ctx.fillStyle = '#6b7280';
- ctx.textAlign = 'center';
- ctx.fillText('-', x + cellWidth/2, y + cellHeight/2 + 4);
- }
-
- // Remettre la police normale après
- ctx.font = '12px Arial';
- }
- }
-
- // Labels des étudiants (vertical, en haut)
- ctx.fillStyle = '#374151';
- ctx.textAlign = 'left';
- for (let i = 0; i < students.length; i++) {
- const x = padding + i * cellWidth + cellWidth/2;
- const y = padding - 10;
-
- ctx.save();
- ctx.translate(x, y);
- ctx.rotate(-Math.PI/4); // Rotation de -45 degrés
- ctx.fillText(students[i], 0, 0);
- ctx.restore();
- }
-
- // Labels des catégories (à gauche)
- ctx.textAlign = 'right';
- ctx.font = type === 'élément' ? '10px Arial' : '12px Arial'; // Police plus petite pour les éléments
-
- for (let i = 0; i < categories.length; i++) {
- const x = padding - 10;
- const y = padding + i * cellHeight + cellHeight/2 + 4;
-
- // Utiliser la couleur de la catégorie si disponible
- const category = categories[i];
- ctx.fillStyle = colors[category] || '#374151';
-
- // Tronquer le texte si trop long pour les éléments
- let displayText = category;
- if (type === 'élément' && category.length > 25) {
- displayText = category.substring(0, 22) + '...';
- }
-
- ctx.fillText(displayText, x, y);
- }
-
- // Légende (échelle de couleur continue)
- const legendY = canvas.height - 40;
- const legendHeight = 20;
- const legendWidth = 200;
- const legendX = (canvas.width - legendWidth) / 2;
-
- // Dessiner la barre de légende avec dégradé continu
- for (let i = 0; i <= 100; i++) {
- const x = legendX + (i / 100) * legendWidth;
- ctx.fillStyle = getScoreColor(i);
- ctx.fillRect(x, legendY, 2, legendHeight);
- }
-
- // Bordure de la légende
- ctx.strokeStyle = '#d1d5db';
- ctx.strokeRect(legendX, legendY, legendWidth, legendHeight);
-
- // Labels de la légende
- ctx.fillStyle = '#374151';
- ctx.textAlign = 'center';
- ctx.fillText('0%', legendX, legendY + legendHeight + 15);
- ctx.fillText('50%', legendX + legendWidth/2, legendY + legendHeight + 15);
- ctx.fillText('100%', legendX + legendWidth, legendY + legendHeight + 15);
-
- // Titre de la légende
- ctx.fillText('Score moyen (%)', legendX + legendWidth/2, legendY - 10);
- }
-
- function createGradingElementsHeatmap(canvasId, data) {
- const canvas = document.getElementById(canvasId);
- const ctx = canvas.getContext('2d');
-
- const students = data.students;
- const elements = data.elements;
- const detailedScores = data.detailed_scores;
- const colors = data.colors;
- const scaleColors = data.scale_colors;
-
- if (!elements || elements.length === 0) return;
-
- // Dimensions adaptées pour les éléments
- const padding = 120;
- const minCellWidth = 50;
- const minCellHeight = 30;
-
- const cellWidth = Math.max(minCellWidth, (canvas.width - padding * 2) / students.length);
- const cellHeight = Math.max(minCellHeight, (canvas.height - padding * 2) / elements.length);
-
- // Ajuster la taille du canvas si nécessaire
- canvas.width = cellWidth * students.length + padding * 2;
- canvas.height = cellHeight * elements.length + padding * 2;
-
- // Effacer le canvas
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- ctx.font = '10px Arial';
-
- // Fonction pour obtenir la couleur selon le type et la valeur
- function getGradeColor(gradeData) {
- if (!gradeData) {
- return '#f3f4f6'; // Gris clair pour les données manquantes
- }
-
- const { value, grading_type, max_points } = gradeData;
-
- // Pour les valeurs spéciales définies en base (., d, a, etc.)
- if (scaleColors[value]) {
- return scaleColors[value].color;
- }
-
- if (grading_type === 'score') {
- // Pour les scores (0-3), utiliser directement les couleurs de base
- if (scaleColors[value]) {
- return scaleColors[value].color;
- }
- // Fallback si la valeur n'existe pas dans l'échelle
- return '#6b7280';
- } else if (grading_type === 'notes') {
- // Pour les notes numériques, calculer le pourcentage et utiliser un dégradé
- try {
- const numericValue = parseFloat(value.toString().replace(',', '.'));
- const percentage = (numericValue / max_points) * 100;
-
- // Utiliser le même dégradé HSL que les autres heatmaps pour les notes
- const factor = Math.max(0, Math.min(100, percentage)) / 100;
- return interpolateColorHSL('#ef4444', '#22c55e', factor);
- } catch (e) {
- return '#6b7280'; // Gris si erreur de parsing
- }
- }
-
- return '#6b7280'; // Couleur par défaut
- }
-
- // Dessiner les cellules
- for (let studentIndex = 0; studentIndex < students.length; studentIndex++) {
- for (let elementIndex = 0; elementIndex < elements.length; elementIndex++) {
- const gradeData = detailedScores[studentIndex][elementIndex];
- const x = padding + studentIndex * cellWidth;
- const y = padding + elementIndex * cellHeight;
-
- // Couleur de fond basée sur la vraie valeur et le type
- ctx.fillStyle = getGradeColor(gradeData);
- ctx.fillRect(x, y, cellWidth - 1, cellHeight - 1);
-
- // Bordure
- ctx.strokeStyle = '#d1d5db';
- ctx.strokeRect(x, y, cellWidth - 1, cellHeight - 1);
-
- // Texte de la valeur originale
- ctx.font = '10px Arial';
- if (gradeData) {
- // Déterminer la couleur du texte selon la luminosité du fond
- const bgColor = getGradeColor(gradeData);
- const isLightBackground = isColorLight(bgColor);
- ctx.fillStyle = isLightBackground ? '#000000' : '#ffffff';
- ctx.textAlign = 'center';
-
- // Afficher la valeur originale (pas de conversion en %)
- ctx.fillText(gradeData.value.toString(), x + cellWidth/2, y + cellHeight/2 + 4);
- } else {
- ctx.fillStyle = '#6b7280';
- ctx.textAlign = 'center';
- ctx.fillText('-', x + cellWidth/2, y + cellHeight/2 + 4);
- }
- }
- }
-
- // Labels des étudiants (vertical, en haut)
- ctx.fillStyle = '#374151';
- ctx.textAlign = 'left';
- ctx.font = '12px Arial';
- for (let i = 0; i < students.length; i++) {
- const x = padding + i * cellWidth + cellWidth/2;
- const y = padding - 10;
-
- ctx.save();
- ctx.translate(x, y);
- ctx.rotate(-Math.PI/4);
- ctx.fillText(students[i], 0, 0);
- ctx.restore();
- }
-
- // Labels des éléments (à gauche)
- ctx.textAlign = 'right';
- ctx.font = '10px Arial';
-
- for (let i = 0; i < elements.length; i++) {
- const x = padding - 10;
- const y = padding + i * cellHeight + cellHeight/2 + 4;
-
- // Utiliser la couleur de l'élément si disponible
- const element = elements[i];
- ctx.fillStyle = colors[element] || '#374151';
-
- // Tronquer le texte si trop long
- let displayText = element;
- if (element.length > 25) {
- displayText = element.substring(0, 22) + '...';
- }
-
- ctx.fillText(displayText, x, y);
- }
-
- // Légende spéciale pour les éléments de notation
- drawGradingElementsLegend(ctx, canvas, scaleColors);
- }
-
- // Fonction pour déterminer si une couleur est claire ou foncée
- function isColorLight(hexColor) {
- const rgb = hexToRgb(hexColor);
- if (!rgb) return true;
-
- // Calcul de la luminance relative
- const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
- return luminance > 0.5;
- }
-
- // Fonction pour dessiner la légende spécialisée
- function drawGradingElementsLegend(ctx, canvas, scaleColors) {
- const legendY = canvas.height - 80;
- const legendItemHeight = 15;
- const legendItemWidth = 15;
- const legendSpacing = 5;
-
- ctx.font = '11px Arial';
- ctx.textAlign = 'left';
-
- // Titre de la légende
- ctx.fillStyle = '#374151';
- ctx.fillText('Échelle de notation :', 20, legendY - 10);
-
- let currentX = 20;
- let currentY = legendY;
-
- // Afficher les valeurs de l'échelle configurée
- const sortedScaleItems = Object.entries(scaleColors).sort((a, b) => {
- // Trier les valeurs numériques avant les valeurs spéciales
- const aIsNum = !isNaN(parseInt(a[0]));
- const bIsNum = !isNaN(parseInt(b[0]));
-
- if (aIsNum && bIsNum) return parseInt(a[0]) - parseInt(b[0]);
- if (aIsNum && !bIsNum) return -1;
- if (!aIsNum && bIsNum) return 1;
- return a[0].localeCompare(b[0]);
- });
-
- for (const [value, config] of sortedScaleItems) {
- // Rectangle de couleur
- ctx.fillStyle = config.color;
- ctx.fillRect(currentX, currentY, legendItemWidth, legendItemHeight);
-
- // Bordure
- ctx.strokeStyle = '#d1d5db';
- ctx.strokeRect(currentX, currentY, legendItemWidth, legendItemHeight);
-
- // Texte
- ctx.fillStyle = '#374151';
- ctx.fillText(`${value}: ${config.label}`, currentX + legendItemWidth + legendSpacing, currentY + legendItemHeight - 3);
-
- // Passer à la ligne après quelques éléments pour éviter le débordement
- currentX += 120;
- if (currentX > canvas.width - 150) {
- currentX = 20;
- currentY += legendItemHeight + legendSpacing;
- }
- }
-
- // Note explicative pour les notes numériques
- ctx.fillStyle = '#6b7280';
- ctx.font = '10px Arial';
- ctx.fillText('Notes numériques : dégradé rouge (0%) → vert (100%)', 20, currentY + 25);
- }
-
- // === APPELS AUX HEATMAPS (après toutes les définitions de fonctions) ===
-
- {% if heatmap_competences %}
- // Heatmap des compétences
- createHeatmap('competencesHeatmap', competencesData, 'compétence');
- {% endif %}
-
- {% if heatmap_domains %}
- // Heatmap des domaines
- createHeatmap('domainsHeatmap', domainsData, 'domaine');
- {% endif %}
-
- {% if heatmap_grading_elements %}
- // Heatmap des éléments de notation avec couleurs de base
- createGradingElementsHeatmap('gradingElementsHeatmap', gradingElementsData);
- {% endif %}
});
// === FONCTIONNALITÉ D'ENVOI DE BILANS ===
@@ -985,6 +731,95 @@ document.addEventListener('click', function(e) {
closeSendReportsModal();
}
});
+
+// === FONCTIONNALITÉ DE TRI DES TABLEAUX ===
+
+function sortTable(tableId, columnIndex) {
+ const table = document.getElementById(tableId);
+ const tbody = table.querySelector('tbody');
+ const rows = Array.from(tbody.rows);
+ const isNumeric = checkIfNumericColumn(rows, columnIndex);
+
+ // Déterminer la direction du tri
+ const currentDirection = table.dataset.sortDirection || 'none';
+ const newDirection = currentDirection === 'asc' ? 'desc' : 'asc';
+ table.dataset.sortDirection = newDirection;
+ table.dataset.sortColumn = columnIndex;
+
+ // Tri des lignes
+ rows.sort((a, b) => {
+ let aVal = getCellValue(a, columnIndex);
+ let bVal = getCellValue(b, columnIndex);
+
+ if (isNumeric) {
+ aVal = parseFloat(aVal) || 0;
+ bVal = parseFloat(bVal) || 0;
+ } else {
+ aVal = aVal.toLowerCase();
+ bVal = bVal.toLowerCase();
+ }
+
+ if (aVal < bVal) return newDirection === 'asc' ? -1 : 1;
+ if (aVal > bVal) return newDirection === 'asc' ? 1 : -1;
+ return 0;
+ });
+
+ // Réorganiser les lignes dans le tableau
+ rows.forEach(row => tbody.appendChild(row));
+
+ // Mettre à jour les indicateurs visuels
+ updateSortIndicators(table, columnIndex, newDirection);
+}
+
+function getCellValue(row, columnIndex) {
+ const cell = row.cells[columnIndex];
+ if (!cell) return '';
+
+ // Pour les cellules avec des couleurs de fond (valeurs spéciales), extraire le texte
+ let text = cell.textContent.trim();
+
+ // Nettoyer les valeurs numériques (enlever %, /X, etc.)
+ text = text.replace(/[%\/]/g, '').split(/[\s\/]/)[0];
+
+ return text;
+}
+
+function checkIfNumericColumn(rows, columnIndex) {
+ if (rows.length === 0) return false;
+
+ // Vérifier les 5 premières lignes pour déterminer si c'est numérique
+ const sampleSize = Math.min(5, rows.length);
+ let numericCount = 0;
+
+ for (let i = 0; i < sampleSize; i++) {
+ const value = getCellValue(rows[i], columnIndex);
+ if (value && !isNaN(parseFloat(value))) {
+ numericCount++;
+ }
+ }
+
+ return numericCount >= sampleSize / 2;
+}
+
+function updateSortIndicators(table, columnIndex, direction) {
+ // Réinitialiser tous les indicateurs
+ const headers = table.querySelectorAll('th.sortable');
+ headers.forEach(header => {
+ const arrow = header.querySelector('.sort-arrow');
+ if (arrow) {
+ arrow.textContent = '⇅';
+ }
+ });
+
+ // Mettre à jour l'indicateur de la colonne triée
+ const currentHeader = headers[columnIndex];
+ if (currentHeader) {
+ const arrow = currentHeader.querySelector('.sort-arrow');
+ if (arrow) {
+ arrow.textContent = direction === 'asc' ? '▲' : '▼';
+ }
+ }
+}
|---|