Moodle Grade Distribution Graph // Gràfica de distribució de les notes

Calculates grading statistics and displays a grade distribution graph for Moodle assignments

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Moodle Grade Distribution Graph // Gràfica de distribució de les notes
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  Calculates grading statistics and displays a grade distribution graph for Moodle assignments
// @author       Ermengol Bota
// @match        *://*/mod/assign/view.php?id=*&action=grading
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('=== Script Suma Qualificacions iniciat ===');

    // Funció per calcular estadístiques
    function calcularEstadistiques(notes) {
        console.log('Calculant estadístiques per notes:', notes);

        if (notes.length === 0) {
            return {
                mitjana: 0,
                mediana: 0,
                moda: 0,
                desviacio: 0,
                minim: 0,
                maxim: 0
            };
        }

        // Mitjana
        const mitjana = notes.reduce((sum, val) => sum + val, 0) / notes.length;
        console.log('Mitjana:', mitjana);

        // Mediana
        const notesOrdenades = [...notes].sort((a, b) => a - b);
        let mediana;
        if (notesOrdenades.length % 2 === 0) {
            mediana = (notesOrdenades[notesOrdenades.length / 2 - 1] + notesOrdenades[notesOrdenades.length / 2]) / 2;
        } else {
            mediana = notesOrdenades[Math.floor(notesOrdenades.length / 2)];
        }
        console.log('Mediana:', mediana);

        // Moda (valor més freqüent)
        const frequencia = {};
        notes.forEach(nota => {
            frequencia[nota] = (frequencia[nota] || 0) + 1;
        });
        let maxFreq = 0;
        let moda = notes[0];
        for (const [valor, freq] of Object.entries(frequencia)) {
            if (freq > maxFreq) {
                maxFreq = freq;
                moda = parseFloat(valor);
            }
        }
        console.log('Moda:', moda, 'amb freqüència:', maxFreq);

        // Desviació estàndard
        const variancia = notes.reduce((sum, val) => sum + Math.pow(val - mitjana, 2), 0) / notes.length;
        const desviacio = Math.sqrt(variancia);
        console.log('Desviació estàndard:', desviacio);

        // Mínim i màxim
        const minim = Math.min(...notes);
        const maxim = Math.max(...notes);
        console.log('Mínim:', minim, 'Màxim:', maxim);

        return {
            mitjana,
            mediana,
            moda,
            desviacio,
            minim,
            maxim
        };
    }

    // Funció per calcular la suma
    function calcularSuma() {
        console.log('Calculant suma...');

        // Detectar si són inputs o selects
        const inputs = document.querySelectorAll('input.quickgrade[type="text"]');
        const selects = document.querySelectorAll('select.quickgrade');

        const esSelect = selects.length > 0;
        console.log('Tipus detectat:', esSelect ? 'SELECT' : 'INPUT');
        console.log('Nombre d\'elements trobats:', esSelect ? selects.length : inputs.length);

        if (esSelect) {
            // Processar selects
            return processarSelects(selects);
        } else {
            // Processar inputs numèrics
            return processarInputs(inputs);
        }
    }

    // Funció per processar selects
    function processarSelects(selects) {
        const opcions = [];
        const comptador = {};
        let senseNota = 0;

        // Obtenir totes les opcions possibles del primer select
        if (selects.length > 0) {
            const primeraSelect = selects[0];
            Array.from(primeraSelect.options).forEach(option => {
                if (option.value !== "-1") { // Ignorar "Sense qualificació"
                    opcions.push({
                        value: option.value,
                        text: option.text
                    });
                    comptador[option.value] = 0;
                }
            });
            console.log('Opcions detectades:', opcions);
        }

        // Comptar quants alumnes tenen cada opció
        selects.forEach((select, index) => {
            const valorSeleccionat = select.value;
            console.log(`Select ${index + 1}: valor="${valorSeleccionat}"`);

            if (valorSeleccionat === "-1") {
                senseNota++;
            } else if (comptador.hasOwnProperty(valorSeleccionat)) {
                comptador[valorSeleccionat]++;
            }
        });

        console.log('Comptador final:', comptador);
        console.log('Alumnes sense nota:', senseNota);

        return {
            esSelect: true,
            totalInputs: selects.length,
            opcions: opcions,
            comptador: comptador,
            senseNota: senseNota
        };
    }

    // Funció per processar inputs numèrics
    function processarInputs(inputs) {

        let sumaTots = 0;
        let sumaNoZero = 0;
        let comptadorNoZero = 0;
        let notesTotes = [];
        let notesNoZero = [];
        let notaMaxima = null;

        inputs.forEach((input, index) => {
            // Buscar la nota màxima del primer input
            if (index === 0 && !notaMaxima) {
                const nextSibling = input.nextSibling;
                if (nextSibling && nextSibling.textContent) {
                    const match = nextSibling.textContent.match(/\/\s*(\d+(?:,\d+)?)/);
                    if (match) {
                        notaMaxima = parseFloat(match[1].replace(',', '.'));
                        console.log('Nota màxima detectada:', notaMaxima);
                    }
                }
            }

            const valor = input.value.trim();
            console.log(`Input ${index + 1}: id="${input.id}", valor="${valor}"`);

            if (valor !== '') {
                // Convertir comes a punts per parsejar correctament
                const numero = parseFloat(valor.replace(',', '.'));
                if (!isNaN(numero)) {
                    // Sumar sempre (incloent zeros) per la mitjana de tots
                    sumaTots += numero;
                    notesTotes.push(numero);
                    console.log(`  -> Valor numèric: ${numero}, Suma total acumulada: ${sumaTots}`);

                    // Si no és zero, també comptar per la mitjana sense zeros
                    if (numero !== 0) {
                        sumaNoZero += numero;
                        comptadorNoZero++;
                        notesNoZero.push(numero);
                        console.log(`  -> No és zero, suma sense zeros: ${sumaNoZero}, comptador: ${comptadorNoZero}`);
                    } else {
                        console.log(`  -> És zero, no es compta per la mitjana sense zeros`);
                    }
                } else {
                    console.log(`  -> No és un número vàlid`);
                }
            } else {
                console.log(`  -> Camp buit, es compta com 0 per la mitjana de tots`);
                notesTotes.push(0);
            }
        });

        console.log(`Resultat final: SumaTots=${sumaTots}, TotalInputs=${inputs.length}, SumaNoZero=${sumaNoZero}, ComptadorNoZero=${comptadorNoZero}`);
        return {
            esSelect: false,
            sumaTots,
            totalInputs: inputs.length,
            sumaNoZero,
            comptadorNoZero,
            notesTotes,
            notesNoZero,
            notaMaxima
        };
    }

    // Funció per mostrar el resultat
    function mostrarResultat() {
        console.log('Intentant mostrar resultat...');

        // Buscar el div de navegació terciària
        const navDiv = document.querySelector('div.container-fluid.tertiary-navigation');

        if (!navDiv) {
            console.error('ERROR: No s\'ha trobat el div de navegació terciària');
            console.log('Divs container-fluid trobats:', document.querySelectorAll('div.container-fluid').length);
            return;
        }

        console.log('Div de navegació trobat:', navDiv);

        // Comprovar si ja existeix el div de resultats
        let resultatDiv = document.getElementById('suma-qualificacions');

        if (!resultatDiv) {
            console.log('Creant nou div de resultats...');
            // Crear el div de resultats
            resultatDiv = document.createElement('div');
            resultatDiv.id = 'suma-qualificacions';
            resultatDiv.style.cssText = 'margin-top: 15px; padding: 15px; background-color: #d4edda; border: 1px solid #c3e6cb; border-radius: 5px; color: #155724;';

            // Inserir després del div de navegació
            navDiv.parentNode.insertBefore(resultatDiv, navDiv.nextSibling);
            console.log('Div de resultats creat i inserit');
        } else {
            console.log('Div de resultats ja existeix, actualitzant...');
        }

        // Calcular la suma/comptador
        const resultats = calcularSuma();

        if (resultats.esSelect) {
            // Mostrar resultats per selects
            mostrarResultatsSelect(resultatDiv, resultats);
        } else {
            // Mostrar resultats per inputs numèrics
            mostrarResultatsInput(resultatDiv, resultats);
        }

        console.log('Contingut actualitzat correctament');
    }

    // Funció per mostrar resultats de selects
    function mostrarResultatsSelect(resultatDiv, resultats) {
        const { totalInputs, opcions, comptador, senseNota } = resultats;

        // Trobar el valor màxim per escalar les barres
        const maxCount = Math.max(...Object.values(comptador));
        const alturaMaxBarra = 200;

        let htmlGrafica = '<strong>Distribució de qualificacions:</strong><br>';
        htmlGrafica += '<div style="display: flex; align-items: flex-end; height: ' + (alturaMaxBarra + 60) + 'px; gap: 10px; margin-top: 10px; padding: 10px; background: #ffffff; border-radius: 5px; border: 1px solid #dee2e6;">';

        opcions.forEach(opcio => {
            const count = comptador[opcio.value] || 0;
            const alturaBarra = maxCount > 0 ? (count / maxCount) * alturaMaxBarra : 0;
            const colorBarra = count > 0 ? '#007bff' : '#dee2e6';

            htmlGrafica += `
                <div style="flex: 1; display: flex; flex-direction: column; align-items: center;">
                    <div style="font-weight: bold; font-size: 14px; margin-bottom: 5px; color: #333;">${count}</div>
                    <div style="width: 100%; height: ${alturaBarra}px; background-color: ${colorBarra}; border-radius: 3px 3px 0 0;" title="${opcio.text}: ${count} alumnes"></div>
                    <div style="font-size: 11px; margin-top: 5px; text-align: center; color: #666; word-wrap: break-word; max-width: 100%;">${opcio.text}</div>
                </div>
            `;
        });

        htmlGrafica += '</div>';

        const textSenseNota = senseNota > 0 ? ` (incloent ${senseNota} alumnes sense nota)` : '';

        resultatDiv.innerHTML = `
            <strong>Total alumnes: ${totalInputs}${textSenseNota}</strong><br>
            ${htmlGrafica}
        `;
    }

    // Funció per mostrar resultats de inputs numèrics
    function mostrarResultatsInput(resultatDiv, resultats) {

        // Calcular la suma
        const { sumaTots, totalInputs, sumaNoZero, comptadorNoZero, notesTotes, notesNoZero, notaMaxima } = resultats;
        const mitjanaTots = totalInputs > 0 ? (sumaTots / totalInputs).toFixed(2) : '0.00';
        const mitjanaNoZero = comptadorNoZero > 0 ? (sumaNoZero / comptadorNoZero).toFixed(2) : '0.00';

        // Calcular estadístiques només amb notes > 0
        const stats = calcularEstadistiques(notesNoZero);

        // Calcular intervals si tenim nota màxima
        let htmlIntervals = '';
        if (notaMaxima && notaMaxima > 0) {
            const numIntervals = 10;
            const amplada = notaMaxima / numIntervals;
            const intervals = new Array(numIntervals).fill(0);

            console.log(`Creant ${numIntervals} intervals d'amplada ${amplada}`);

            notesTotes.forEach(nota => {
                let interval = Math.floor(nota / amplada);
                if (interval >= numIntervals) interval = numIntervals - 1; // Per notes exactament iguals a la màxima
                intervals[interval]++;
                console.log(`Nota ${nota} -> Interval ${interval}`);
            });

            // Trobar el valor màxim per escalar les barres
            const maxCount = Math.max(...intervals);
            const alturaMaxBarra = 200; // píxels

            htmlIntervals = '<br><strong>Distribució de les notes (incloent-hi zeros i no presentats):</strong><br>';
            htmlIntervals += '<div style="display: flex; align-items: flex-end; height: ' + (alturaMaxBarra + 60) + 'px; gap: 5px; margin-top: 10px; padding: 10px; background: #ffffff; border-radius: 5px; border: 1px solid #dee2e6;">';

            for (let i = 0; i < numIntervals; i++) {
                const min = (i * amplada).toFixed(1);
                const max = ((i + 1) * amplada).toFixed(1);
                const count = intervals[i];
                const alturaBarra = maxCount > 0 ? (count / maxCount) * alturaMaxBarra : 0;
                const colorBarra = count > 0 ? '#007bff' : '#dee2e6';

                htmlIntervals += `
                    <div style="flex: 1; display: flex; flex-direction: column; align-items: center;">
                        <div style="font-weight: bold; font-size: 12px; margin-bottom: 5px; color: #333;">${count}</div>
                        <div style="width: 100%; height: ${alturaBarra}px; background-color: ${colorBarra}; border-radius: 3px 3px 0 0; transition: background-color 0.3s;" title="${min} - ${max}: ${count} notes"></div>
                        <div style="font-size: 10px; margin-top: 5px; text-align: center; color: #666;">${min}-${max}</div>
                    </div>
                `;
            }

            htmlIntervals += '</div>';
        }

        // Actualitzar el contingut
        resultatDiv.innerHTML = `
            <strong>Resum de qualificacions:</strong><br>
            <span title="Suma de totes les notes dividida pel nombre de notes. Representa el valor central del conjunt de dades." style="cursor: help; border-bottom: 1px dotted #666;">Mitjana</span>: <strong>${mitjanaNoZero}</strong> (${comptadorNoZero} alumnes)<br>
            <span title="Suma de totes les notes dividida pel nombre de notes. Representa el valor central del conjunt de dades." style="cursor: help; border-bottom: 1px dotted #666;">Mitjana</span> total: <strong>${mitjanaTots}</strong> (${totalInputs} alumnes, contant els 0 i els no presentats)<br>
            <br>
            <strong>Estadístiques (sense zeros ni no presentats):</strong><br>
            <span title="Valor que divideix les notes en dues meitats iguals. És la nota que queda al mig quan s'ordenen totes de menor a major. Menys sensible a valors extrems que la mitjana." style="cursor: help; border-bottom: 1px dotted #666;">Mediana</span>: <strong>${stats.mediana.toFixed(2)}</strong> |
            <span title="Nota que més es repeteix entre tots els alumnes. Indica quin valor és més freqüent al grup." style="cursor: help; border-bottom: 1px dotted #666;">Moda</span>: <strong>${stats.moda.toFixed(2)}</strong> |
            <span title="Mesura la dispersió de les notes respecte la mitjana. Un valor baix indica que les notes estan molt agrupades (grup homogeni). Un valor alt indica que hi ha molta variabilitat (grup heterogeni)." style="cursor: help; border-bottom: 1px dotted #666;">Desv. estàndard</span>: <strong>${stats.desviacio.toFixed(2)}</strong><br>
            Mínim: <strong>${stats.minim.toFixed(2)}</strong> |
            Màxim: <strong>${stats.maxim.toFixed(2)}</strong>
            ${htmlIntervals}
        `;

        console.log('Contingut actualitzat correctament');
    }
    window.addEventListener('load', function() {
        console.log('Pàgina carregada, esperant 0,5 segons...');
        setTimeout(function() {
            console.log('Timeout completat, executant mostrarResultat()');
            mostrarResultat();
            console.log('=== Script finalitzat ===');
        }, 500);
    });
})();