diff --git a/apps/backend/src/modules/grade-distribution/controller.ts b/apps/backend/src/modules/grade-distribution/controller.ts index ec376fdef..dfc075f69 100644 --- a/apps/backend/src/modules/grade-distribution/controller.ts +++ b/apps/backend/src/modules/grade-distribution/controller.ts @@ -300,14 +300,18 @@ export const getGradeDistributionByInstructorAndSemester = async ( "class.session.term.name": name, }); + const terms = await TermModel.find({ + name: name, + }); + if (sections.length === 0) throw new Error("No classes found"); const distributions = await GradeDistributionModel.find({ classNumber: { $in: sections.map((section) => section.id) }, + termId: { $in: terms.map((term) => term.id) }, }); - // if (distributions.length === 0) - // throw new Error("No grade distributions found"); + if (distributions.length === 0) throw new Error("No grades found"); const distribution = getDistribution(distributions); diff --git a/apps/backend/src/modules/grade-distribution/typedefs/grade-distribution.ts b/apps/backend/src/modules/grade-distribution/typedefs/grade-distribution.ts index d18835b87..a44d162f5 100644 --- a/apps/backend/src/modules/grade-distribution/typedefs/grade-distribution.ts +++ b/apps/backend/src/modules/grade-distribution/typedefs/grade-distribution.ts @@ -3,7 +3,7 @@ import { gql } from "graphql-tag"; export default gql` type GradeDistribution @cacheControl(maxAge: 1) { average: Float - distribution: [Grade!]! + distribution: [Grade!] } type Grade @cacheControl(maxAge: 1) { diff --git a/apps/frontend/src/app/GradeDistributions/GradeDistributions.module.scss b/apps/frontend/src/app/GradeDistributions/GradeDistributions.module.scss index 763627117..d27051527 100644 --- a/apps/frontend/src/app/GradeDistributions/GradeDistributions.module.scss +++ b/apps/frontend/src/app/GradeDistributions/GradeDistributions.module.scss @@ -15,11 +15,27 @@ flex-direction: column; overflow: auto; gap: 24px; + position: relative; .grid { display: grid; gap: 24px; grid-template-columns: repeat(auto-fit, minmax(384px, 1fr)); } + .legend { + margin-left: 30px; + color: var(--paragraph-color); + } + } + .empty { + position: absolute; + color: var(--label-color); + text-align: center; + font-size: 14px; + left: 0; + right: 0; + top: 25vh; + margin-inline: auto; + width: fit-content; } } diff --git a/apps/frontend/src/app/GradeDistributions/HoverInfo/HoverInfo.module.scss b/apps/frontend/src/app/GradeDistributions/HoverInfo/HoverInfo.module.scss new file mode 100644 index 000000000..d5bf308ee --- /dev/null +++ b/apps/frontend/src/app/GradeDistributions/HoverInfo/HoverInfo.module.scss @@ -0,0 +1,28 @@ +.info { + display: inline-block; + width: 250px; + + .heading { + margin-bottom: 5px; + + .color { + display: inline-block; + width: 12px; + height: 12px; + margin-right: 10px; + } + + .course { + color: var(--heading-color); + font-size: 18px; + } + } + .label { + font-family: Inter, sans-serif; + font-weight: 700; + color: var(--heading-color); + margin-top: 8px; + margin-bottom: 4px; + font-size: 14px; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/GradeDistributions/HoverInfo/index.tsx b/apps/frontend/src/app/GradeDistributions/HoverInfo/index.tsx new file mode 100644 index 000000000..8b5b79868 --- /dev/null +++ b/apps/frontend/src/app/GradeDistributions/HoverInfo/index.tsx @@ -0,0 +1,168 @@ +import { useMemo } from "react"; + +import { AverageGrade, ColoredGrade } from "@/components/AverageGrade"; +import { useReadCourseGradeDist } from "@/hooks/api"; +import { GradeDistribution, Semester } from "@/lib/api"; + +import styles from "./HoverInfo.module.scss"; + +interface HoverInfoProps { + color: string; + subject: string; + courseNumber: string; + gradeDistribution: GradeDistribution; + givenName?: string; + familyName?: string; + semester?: Semester; + year?: number; + hoveredLetter: string | null; +} + +const GRADE_STYLE = { display: "inline-block", marginRight: "4px" }; +const GRADE_ORDER = [ + "A+", + "A", + "A-", + "B+", + "B", + "B-", + "C+", + "C", + "C-", + "D+", + "D", + "D-", + "F", +]; + +function addOrdinalSuffix(n: string) { + if (n === "11" || n === "12" || n === "13") return n + "th"; + + switch (n.charAt(n.length - 1)) { + case "1": + return n + "st"; + case "2": + return n + "nd"; + case "3": + return n + "rd"; + default: + return n + "th"; + } +} + +export default function HoverInfo({ + color, + subject, + courseNumber, + gradeDistribution, + givenName, + familyName, + semester, + year, + hoveredLetter, +}: HoverInfoProps) { + const { data: courseData } = useReadCourseGradeDist(subject, courseNumber); + + const courseGradeDist = useMemo( + () => courseData?.gradeDistribution ?? null, + [courseData] + ); + + const { + lower: lowerPercentile, + upper: upperPercentile, + count: hoveredCount, + total: gradeDistTotal, + } = useMemo(() => { + const ret: { + lower: string | null; + upper: string | null; + count: number | null; + total: number; + } = { lower: null, upper: null, count: null, total: 0 }; + if (!gradeDistribution || !hoveredLetter) return ret; + ret.total = gradeDistribution.distribution.reduce( + (acc, g) => acc + g.count, + 0 + ); + if (hoveredLetter === "NP" || hoveredLetter === "P") + return { + lower: "N/A", + upper: "N/A", + count: + gradeDistribution.distribution.find((g) => g.letter === hoveredLetter) + ?.count ?? 0, + total: ret.total, + }; + GRADE_ORDER.reduce((acc, grade) => { + if (grade === hoveredLetter) + ret.upper = addOrdinalSuffix( + (((ret.total - acc) * 100) / ret.total).toFixed(0) + ); + const count = + gradeDistribution.distribution.find((g) => g.letter === grade)?.count ?? + 0; + acc += count; + if (grade === hoveredLetter) { + ret.lower = addOrdinalSuffix( + (((ret.total - acc) * 100) / ret.total).toFixed(0) + ); + ret.count = count; + } + return acc; + }, 0); + return ret; + }, [hoveredLetter, gradeDistribution]); + + return ( +