All files / src/app/student-page/page/dashboard/components capChartCategories.js

100% Statements 20/20
100% Branches 13/13
100% Functions 4/4
100% Lines 17/17

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56      3x                                                     18x   7x 1068x 1103x 84x   7x 7x 7x 7x 984x 984x 984x 984x 984x   7x           18x    
// Allan Ninal 2026-10-04: cap chart categories (defence in depth after the 2,333-class freeze, eruditiontx-services-mvp #440).
 
/** Maximum number of per-class categories drawn before the remainder is folded into "Other (n)". */
export const MAX_CHART_CATEGORIES = 12;
 
/**
 * @typedef {Object} ChartCategory
 * @property {string} label   Category label (class title and section).
 * @property {number} count   Submission count for the class.
 * @property {number} grade   Average grade of the class (already averaged over its graded submissions).
 * @property {number} [weight] Weight of `grade` when averaging (number of graded submissions). Defaults to `count`.
 */
 
/**
 * Cap a list of chart categories so a pathological payload cannot create thousands of bars.
 *
 * With `max` or fewer items the input is returned untouched (same array). Otherwise the `max` most relevant
 * classes are kept and every other class is folded into ONE trailing category labelled `Other (n)`.
 * Relevance = highest submission count (the chart is about submissions and grades, so the busiest classes
 * matter most); ties keep their original order (stable sort). Kept classes are ordered by that relevance.
 *
 * Aggregation for "Other": `count` is the sum; `grade` is a WEIGHTED average (sum(grade*weight)/sum(weight)),
 * never an average of averages; it is 0 when the folded classes have no weight, matching the chart's
 * existing "no graded work" value. The returned `weight` is the summed weight.
 *
 * @param {ChartCategory[]} items
 * @param {number} [max=MAX_CHART_CATEGORIES]
 * @returns {ChartCategory[]} at most `max + 1` items
 */
export function capChartCategories(items, max = MAX_CHART_CATEGORIES) {
    if (!Array.isArray(items) || items.length <= max) return items ?? [];
 
    const ranked = items
        .map((item, index) => ({ item, index }))
        .sort((a, b) => b.item.count - a.item.count || a.index - b.index);
    const kept = ranked.slice(0, max).map((r) => r.item);
 
    let count = 0;
    let weight = 0;
    let weightedGradeSum = 0;
    for (let i = max; i < ranked.length; i++) {
        const { count: c, grade, weight: w } = ranked[i].item;
        const itemWeight = w ?? c;
        count += c;
        weight += itemWeight;
        weightedGradeSum += grade * itemWeight;
    }
    const other = {
        label: `Other (${ranked.length - max})`,
        count,
        weight,
        grade: weight > 0 ? parseFloat((weightedGradeSum / weight).toFixed(2)) : 0,
    };
    return [...kept, other];
}