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];
}
|