From 9ca24551c3c2c79dc7731223999487b41c61797c Mon Sep 17 00:00:00 2001 From: Matthew Rowland Date: Wed, 30 Oct 2024 16:00:58 -0700 Subject: [PATCH] feat: Relationships, start grade distribution chart --- apps/backend/src/modules/class/formatter.ts | 10 +- apps/backend/src/modules/course/formatter.ts | 13 +- .../modules/grade-distribution/controller.ts | 10 +- .../backend/src/modules/schedule/formatter.ts | 13 +- apps/backend/src/modules/user/formatter.ts | 10 +- .../src/components/Class/Grades/index.tsx | 138 +++++++++++------- apps/frontend/src/lib/api/classes.ts | 58 +++++++- apps/frontend/src/lib/api/courses.ts | 40 ----- 8 files changed, 171 insertions(+), 121 deletions(-) diff --git a/apps/backend/src/modules/class/formatter.ts b/apps/backend/src/modules/class/formatter.ts index 7c6b053a9..e538b276b 100644 --- a/apps/backend/src/modules/class/formatter.ts +++ b/apps/backend/src/modules/class/formatter.ts @@ -7,16 +7,16 @@ import { } from "../../generated-types/graphql"; import { ClassModule } from "./generated-types/module-types"; -export type IntermediateClass = Omit< - ClassModule.Class, - "course" | "term" | "primarySection" | "sections" -> & { +interface Relationships { course: null; term: null; primarySection: null; sections: null; gradeDistribution: null; -}; +} + +export type IntermediateClass = Omit & + Relationships; export const formatDate = (date?: string | number | Date | null) => { if (!date) return date; diff --git a/apps/backend/src/modules/course/formatter.ts b/apps/backend/src/modules/course/formatter.ts index f3e070f75..a5e0873c4 100644 --- a/apps/backend/src/modules/course/formatter.ts +++ b/apps/backend/src/modules/course/formatter.ts @@ -9,15 +9,18 @@ import { import { formatDate } from "../class/formatter"; import { CourseModule } from "./generated-types/module-types"; -export type IntermediateCourse = Omit< - CourseModule.Course, - "classes" | "crossListing" | "requiredCourses" | "gradeDistribution" -> & { +interface Relationships { classes: null; crossListing: string[]; requiredCourses: string[]; gradeDistribution: null; -}; +} + +export type IntermediateCourse = Omit< + CourseModule.Course, + keyof Relationships +> & + Relationships; export function formatCourse(course: CourseType) { return { diff --git a/apps/backend/src/modules/grade-distribution/controller.ts b/apps/backend/src/modules/grade-distribution/controller.ts index 3acc77f8c..498def61f 100644 --- a/apps/backend/src/modules/grade-distribution/controller.ts +++ b/apps/backend/src/modules/grade-distribution/controller.ts @@ -138,21 +138,23 @@ export const points: { [key: string]: number } = { D: 1, "D-": 0.7, "D+": 1.3, + F: 0, }; export const getAverageGrade = (distribution: Grade[]) => { const total = distribution.reduce((acc, { letter, count }) => { - if (points[letter]) return acc + count; + if (Object.keys(points).includes(letter)) return acc + count; - // Ignore letters not included in grade point average + // Ignore letters not included in GPA return acc; }, 0); - // For distributions without a grade point average, return null + // For distributions without a GPA, return null if (total === 0) return null; const weightedTotal = distribution.reduce((acc, { letter, count }) => { - if (points[letter]) return points[letter] * count + acc; + if (Object.keys(points).includes(letter)) + return points[letter] * count + acc; return acc; }, 0); diff --git a/apps/backend/src/modules/schedule/formatter.ts b/apps/backend/src/modules/schedule/formatter.ts index 4fc2b9ef4..0eae73954 100644 --- a/apps/backend/src/modules/schedule/formatter.ts +++ b/apps/backend/src/modules/schedule/formatter.ts @@ -2,13 +2,16 @@ import { ScheduleType } from "@repo/common"; import { ScheduleModule } from "./generated-types/module-types"; +interface Relationships { + classes: ScheduleModule.SelectedClassInput[]; + term: null; +} + export type IntermediateSchedule = Omit< ScheduleModule.Schedule, - "term" | "classes" -> & { - term: null; - classes: ScheduleModule.SelectedClassInput[]; -}; + keyof Relationships +> & + Relationships; export const formatSchedule = async (schedule: ScheduleType) => { return { diff --git a/apps/backend/src/modules/user/formatter.ts b/apps/backend/src/modules/user/formatter.ts index 8be92bc6c..0ccb6ca37 100644 --- a/apps/backend/src/modules/user/formatter.ts +++ b/apps/backend/src/modules/user/formatter.ts @@ -2,13 +2,13 @@ import { UserType } from "@repo/common"; import { UserModule } from "./generated-types/module-types"; -export type IntermediateUser = Omit< - UserModule.User, - "bookmarkedClasses" | "bookmarkedCourses" -> & { +interface Relationships { bookmarkedCourses: UserModule.BookmarkedCourseInput[]; bookmarkedClasses: UserModule.BookmarkedClassInput[]; -}; +} + +export type IntermediateUser = Omit & + Relationships; export const formatUser = (user: UserType) => { return { diff --git a/apps/frontend/src/components/Class/Grades/index.tsx b/apps/frontend/src/components/Class/Grades/index.tsx index 0eaa583c9..4a382814d 100644 --- a/apps/frontend/src/components/Class/Grades/index.tsx +++ b/apps/frontend/src/components/Class/Grades/index.tsx @@ -1,78 +1,104 @@ +import { useMemo } from "react"; + import { Bar, BarChart, CartesianGrid, - LabelList, Legend, ResponsiveContainer, + Tooltip, XAxis, } from "recharts"; +import useClass from "@/hooks/useClass"; +import { Grade } from "@/lib/api"; + import styles from "./Grades.module.scss"; -const data = [ - { - grade: "A", - percentage: 20, - average: 25, - }, - { - grade: "B", - percentage: 15, - average: 20, - }, - { - grade: "C", - percentage: 10, - average: 15, - }, - { - grade: "D", - percentage: 5, - average: 10, - }, - { - grade: "F", - percentage: 2.5, - average: 5, - }, - { - grade: "Pass", - percentage: 35, - average: 20, - }, - { - grade: "Not pass", - percentage: 17.5, - average: 5, - }, +export const points: { [key: string]: number } = { + A: 4, + "A-": 3.7, + "A+": 4, + B: 3, + "B-": 2.7, + "B+": 3.3, + C: 2, + "C-": 1.7, + "C+": 2.3, + D: 1, + "D-": 0.7, + "D+": 1.3, + F: 0, +}; + +const letters = [ + "A+", + "A", + "A-", + "B+", + "B", + "B-", + "C+", + "C", + "C-", + "D", + "F", + "P", + "NP", ]; export default function Grades() { + const { + class: { + gradeDistribution, + course: { gradeDistribution: courseGradeDistribution }, + }, + } = useClass(); + + const data = useMemo(() => { + const getTotal = (distribution: Grade[]) => + distribution.reduce((acc, grade) => acc + grade.count, 0); + + const classTotal = getTotal(gradeDistribution.distribution); + const courseTotal = getTotal(courseGradeDistribution.distribution); + + return letters.map((letter) => { + const getCount = (distribution: Grade[]) => + distribution.find((grade) => grade.letter === letter)?.count || 0; + + return { + letter, + class: getCount(gradeDistribution.distribution) / classTotal, + course: getCount(courseGradeDistribution.distribution) / courseTotal, + }; + }); + }, [gradeDistribution, courseGradeDistribution]); + return (
- - + - - - - - - + + {gradeDistribution.average && ( + + )} + +
diff --git a/apps/frontend/src/lib/api/classes.ts b/apps/frontend/src/lib/api/classes.ts index 38f036e59..5b8248147 100644 --- a/apps/frontend/src/lib/api/classes.ts +++ b/apps/frontend/src/lib/api/classes.ts @@ -1,6 +1,6 @@ import { gql } from "@apollo/client"; -import { ICourse } from "."; +import { GradeDistribution, ICourse } from "."; import { ITerm, Semester } from "./terms"; export enum InstructionMethod { @@ -194,6 +194,7 @@ export interface IClass { primarySection: ISection; sections: ISection[]; term: ITerm; + gradeDistribution: GradeDistribution; // Attributes session: string; @@ -235,11 +236,26 @@ export const READ_CLASS = gql` unitsMin gradingBasis finalExam + gradeDistribution { + average + distribution { + letter + count + } + } course { title description + classes { + year + semester + } gradeDistribution { average + distribution { + letter + count + } } academicCareer requirements @@ -308,3 +324,43 @@ export const READ_CLASS = gql` } } `; + +export interface GetClassesResponse { + catalog: ICourse[]; +} + +export const GET_CLASSES = gql` + query GetClasses($year: Int!, $semester: Semester!) { + catalog(year: $year, semester: $semester) { + subject + number + title + gradeDistribution { + average + } + academicCareer + classes { + subject + courseNumber + number + title + unitsMax + unitsMin + finalExam + gradingBasis + primarySection { + component + online + open + enrollCount + enrollMax + waitlistCount + waitlistMax + meetings { + days + } + } + } + } + } +`; diff --git a/apps/frontend/src/lib/api/courses.ts b/apps/frontend/src/lib/api/courses.ts index d9c81067c..5256cd437 100644 --- a/apps/frontend/src/lib/api/courses.ts +++ b/apps/frontend/src/lib/api/courses.ts @@ -97,43 +97,3 @@ export const GET_COURSES = gql` } } `; - -export interface GetClassesResponse { - catalog: ICourse[]; -} - -export const GET_CLASSES = gql` - query GetClasses($year: Int!, $semester: Semester!) { - catalog(year: $year, semester: $semester) { - subject - number - title - gradeDistribution { - average - } - academicCareer - classes { - subject - courseNumber - number - title - unitsMax - unitsMin - finalExam - gradingBasis - primarySection { - component - online - open - enrollCount - enrollMax - waitlistCount - waitlistMax - meetings { - days - } - } - } - } - } -`;