feat: grades by professor (#225)

* feat: grades by professor

* chore(pr-review): extract into Distributions type

---------

Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
This commit is contained in:
Samuel Gunter
2024-05-27 14:36:42 -07:00
committed by GitHub
parent bc354f3798
commit 78d749a8a9
4 changed files with 69 additions and 42 deletions

View File

@@ -22,6 +22,8 @@ export type CourseSQLRow = {
Course_Number: string; Course_Number: string;
Course_Title: string; Course_Title: string;
Course_Full_Title: string; Course_Full_Title: string;
Instructor_First: string | null;
Instructor_Last: string | null;
A: number; A: number;
A_Minus: number; A_Minus: number;
B_Plus: number; B_Plus: number;

View File

@@ -52,13 +52,14 @@ const GRADE_COLORS = {
*/ */
export default function GradeDistribution({ course }: GradeDistributionProps): JSX.Element { export default function GradeDistribution({ course }: GradeDistributionProps): JSX.Element {
const [semester, setSemester] = useState('Aggregate'); const [semester, setSemester] = useState('Aggregate');
const [distributions, setDistributions] = useState<Record<string, Distribution>>({}); type Distributions = Record<string, { data: Distribution; instructorIncluded: boolean }>;
const [distributions, setDistributions] = useState<Distributions>({});
const [status, setStatus] = useState<DataStatusType>(DataStatus.LOADING); const [status, setStatus] = useState<DataStatusType>(DataStatus.LOADING);
const ref = useRef<HighchartsReact.RefObject>(null); const ref = useRef<HighchartsReact.RefObject>(null);
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (status === DataStatus.FOUND && distributions[semester]) { if (status === DataStatus.FOUND && distributions[semester]) {
return Object.entries(distributions[semester]!).map(([grade, count]) => ({ return Object.entries(distributions[semester]!.data).map(([grade, count]) => ({
y: count, y: count,
color: GRADE_COLORS[grade as LetterGrade], color: GRADE_COLORS[grade as LetterGrade],
})); }));
@@ -69,8 +70,11 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
useEffect(() => { useEffect(() => {
const fetchInitialData = async () => { const fetchInitialData = async () => {
try { try {
const [aggregateDist, semesters] = await queryAggregateDistribution(course); const [aggregateDist, semesters, instructorIncludedAggregate] =
const initialDistributions: Record<string, Distribution> = { Aggregate: aggregateDist }; await queryAggregateDistribution(course);
const initialDistributions: Distributions = {
Aggregate: { data: aggregateDist, instructorIncluded: instructorIncludedAggregate },
};
const semesterPromises = semesters.map(semester => querySemesterDistribution(course, semester)); const semesterPromises = semesters.map(semester => querySemesterDistribution(course, semester));
const semesterDistributions = await Promise.allSettled(semesterPromises); const semesterDistributions = await Promise.allSettled(semesterPromises);
semesters.forEach((semester, i) => { semesters.forEach((semester, i) => {
@@ -81,7 +85,11 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
} }
if (distributionResult.status === 'fulfilled') { if (distributionResult.status === 'fulfilled') {
initialDistributions[`${semester.season} ${semester.year}`] = distributionResult.value; const [distribution, instructorIncluded] = distributionResult.value;
initialDistributions[`${semester.season} ${semester.year}`] = {
data: distribution,
instructorIncluded,
};
} }
}); });
setDistributions(initialDistributions); setDistributions(initialDistributions);
@@ -236,6 +244,14 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
))} ))}
</select> </select>
</div> </div>
{distributions[semester] && !distributions[semester]!.instructorIncluded && (
<div className='mt-3 flex flex-wrap content-center items-center self-stretch justify-center gap-3'>
<Text variant='mini' className='text-theme-red italic!'>
Instructor-specific data is not available for this course
{semester !== 'Aggregate' && ` for ${semester}`}, showing course-wide data instead
</Text>
</div>
)}
<HighchartsReact ref={ref} highcharts={Highcharts} options={chartOptions} /> <HighchartsReact ref={ref} highcharts={Highcharts} options={chartOptions} />
</> </>
)} )}

View File

@@ -3,27 +3,26 @@ import type { CourseSQLRow, Distribution } from '@shared/types/Distribution';
import { initializeDB } from './initializeDB'; import { initializeDB } from './initializeDB';
// TODO: in the future let's maybe refactor this to be reactive to the items in the db rather than being explicit
const allTables = [
'grade_distributions_2019_2020',
'grade_distributions_2020_2021',
'grade_distributions_2021_2022',
'grade_distributions_2022_2023',
] as const;
/** /**
* fetches the aggregate distribution of grades for a given course from the course db, and the semesters that we have data for * fetches the aggregate distribution of grades for a given course from the course db, and the semesters that we have data for
* @param course the course to fetch the distribution for * @param course the course to fetch the distribution for
* @returns a Distribution object containing the distribution of grades for the course, and * @returns a Distribution object containing the distribution of grades for the course, and
* an array of semesters that we have the distribution for * an array of semesters that we have the distribution for
*/ */
export async function queryAggregateDistribution(course: Course): Promise<[Distribution, Semester[]]> { export async function queryAggregateDistribution(course: Course): Promise<[Distribution, Semester[], boolean]> {
const db = await initializeDB(); const db = await initializeDB();
const query = generateQuery(course, null); const query = generateQuery(course, null, true);
const res = db.exec(query)?.[0]; let res = db.exec(query)?.[0];
let instructorIncluded = true;
if (!res?.columns?.length) { if (!res?.columns?.length) {
throw new NoDataError(course); instructorIncluded = false;
const queryWithoutInstructor = generateQuery(course, null, false);
res = db.exec(queryWithoutInstructor)?.[0];
if (!res?.columns?.length) {
throw new NoDataError(course);
}
} }
const row: Required<CourseSQLRow> = {} as Required<CourseSQLRow>; const row: Required<CourseSQLRow> = {} as Required<CourseSQLRow>;
@@ -79,7 +78,7 @@ export async function queryAggregateDistribution(course: Course): Promise<[Distr
semesters.push({ year: parseInt(year, 10), season: season as Semester['season'] }); semesters.push({ year: parseInt(year, 10), season: season as Semester['season'] });
}); });
return [distribution, semesters]; return [distribution, semesters, instructorIncluded];
} }
/** /**
@@ -88,18 +87,19 @@ export async function queryAggregateDistribution(course: Course): Promise<[Distr
* @param semester the semester to fetch the distribution for OR null if we want the aggregate distribution * @param semester the semester to fetch the distribution for OR null if we want the aggregate distribution
* @returns a SQL query string * @returns a SQL query string
*/ */
function generateQuery(course: Course, semester: Semester | null): string { function generateQuery(course: Course, semester: Semester | null, includeInstructor: boolean): string {
// const profName = course.instructors[0]?.fullName; const profName = course.instructors[0]?.lastName;
// eslint-disable-next-line no-nested-ternary
const yearDelta = semester ? (semester.season === 'Fall' ? 0 : -1) : 0;
const query = ` const query = `
select * from ${semester ? `grade_distributions_${semester.year + yearDelta}_${semester.year + yearDelta + 1}` : `(select * from ${allTables.join(' union all select * from ')})`} select * from grade_distributions
where Department_Code = '${course.department}' where Department_Code = '${course.department}'
and Course_Number = '${course.number}' and Course_Number = '${course.number}'
${includeInstructor ? `and Instructor_Last = '${profName}' collate nocase` : ''}
${semester ? `and Semester = '${semester.season} ${semester.year}'` : ''} ${semester ? `and Semester = '${semester.season} ${semester.year}'` : ''}
`; `;
console.log(includeInstructor, { query });
return query; return query;
} }
@@ -109,13 +109,19 @@ function generateQuery(course: Course, semester: Semester | null): string {
* @param semester the semester to fetch the distribution for * @param semester the semester to fetch the distribution for
* @returns a Distribution object containing the distribution of grades for the course * @returns a Distribution object containing the distribution of grades for the course
*/ */
export async function querySemesterDistribution(course: Course, semester: Semester): Promise<Distribution> { export async function querySemesterDistribution(course: Course, semester: Semester): Promise<[Distribution, boolean]> {
const db = await initializeDB(); const db = await initializeDB();
const query = generateQuery(course, semester); const query = generateQuery(course, semester, true);
const res = db.exec(query)?.[0]; let res = db.exec(query)?.[0];
let instructorIncluded = true;
if (!res?.columns?.length) { if (!res?.columns?.length) {
throw new NoDataError(course); instructorIncluded = false;
const queryWithoutInstructor = generateQuery(course, semester, false);
res = db.exec(queryWithoutInstructor)?.[0];
if (!res?.columns?.length) {
throw new NoDataError(course);
}
} }
const row: Required<CourseSQLRow> = {} as Required<CourseSQLRow>; const row: Required<CourseSQLRow> = {} as Required<CourseSQLRow>;
@@ -142,21 +148,24 @@ export async function querySemesterDistribution(course: Course, semester: Semest
} }
} }
return { return [
A: row.A, {
'A-': row.A_Minus, A: row.A,
'B+': row.B_Plus, 'A-': row.A_Minus,
B: row.B, 'B+': row.B_Plus,
'B-': row.B_Minus, B: row.B,
'C+': row.C_Plus, 'B-': row.B_Minus,
C: row.C, 'C+': row.C_Plus,
'C-': row.C_Minus, C: row.C,
'D+': row.D_Plus, 'C-': row.C_Minus,
D: row.D, 'D+': row.D_Plus,
'D-': row.D_Minus, D: row.D,
F: row.F, 'D-': row.D_Minus,
Other: row.Other, F: row.F,
}; Other: row.Other,
},
instructorIncluded,
];
} }
/** /**