Add Grade Distribution Stuff

This commit is contained in:
Abhinav Chadaga
2024-02-18 01:26:38 -06:00
parent c27bf3c390
commit ddfe952a32
2 changed files with 202 additions and 4 deletions

View File

@@ -3,6 +3,7 @@ import React from 'react';
import { Course } from 'src/shared/types/Course';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import CoursePopupDescriptions from './CoursePopupDescriptions';
import CoursePopupGradeDistribution from './CoursePopupGradeDistribution';
import CoursePopupHeadingAndActions from './CoursePopupHeadingAndActions';
interface CoursePopup2Props {
@@ -16,6 +17,7 @@ const CoursePopup = ({ course, activeSchedule, onClose }: CoursePopup2Props) =>
<div className='flex flex-col'>
<CoursePopupHeadingAndActions course={course} onClose={onClose} activeSchedule={activeSchedule} />
<CoursePopupDescriptions lines={course.description} />
<CoursePopupGradeDistribution course={course} />
</div>
</Popup>
);

View File

@@ -1,7 +1,203 @@
interface CoursePopupGradeDistributionProps {}
import Spinner from '@views/components/common/Spinner/Spinner';
import Text from '@views/components/common/Text/Text';
import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import React from 'react';
import { Course } from 'src/shared/types/Course';
import { Distribution, LetterGrade } from 'src/shared/types/Distribution';
import {
NoDataError,
queryAggregateDistribution,
querySemesterDistribution,
} from 'src/views/lib/database/queryDistribution';
import colors from 'src/views/styles/colors.module.scss';
const CoursePopupGradeDistribution: React.FC<
CoursePopupGradeDistributionProps
> = ({}: CoursePopupGradeDistributionProps) => {};
interface CoursePopupGradeDistributionProps {
course: Course;
}
enum DataStatus {
LOADING = 'LOADING',
FOUND = 'FOUND',
NOT_FOUND = 'NOT_FOUND',
ERROR = 'ERROR',
}
const GRADE_COLORS: Record<LetterGrade, string> = {
A: colors.turtle_pond,
'A-': colors.turtle_pond,
'B+': colors.cactus,
B: colors.cactus,
'B-': colors.cactus,
'C+': colors.sunshine,
C: colors.sunshine,
'C-': colors.sunshine,
'D+': colors.tangerine,
D: colors.tangerine,
'D-': colors.tangerine,
F: colors.speedway_brick,
};
interface State {
semester: string;
distributions: Record<string, Distribution>;
status: DataStatus;
chartData: { y: number; color: string | null }[];
}
type Action =
| { type: 'SET_SEMESTER'; semester: string }
| { type: 'SET_DISTRIBUTIONS'; distributions: Record<string, Distribution> }
| { type: 'SET_STATUS'; status: DataStatus }
| { type: 'SET_CHART_DATA'; chartData: { y: number; color: string | null }[] };
const initialState: State = {
semester: 'Aggregate',
distributions: {},
status: DataStatus.LOADING,
chartData: [],
};
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_SEMESTER':
return { ...state, semester: action.semester };
case 'SET_DISTRIBUTIONS':
return { ...state, distributions: action.distributions };
case 'SET_STATUS':
return { ...state, status: action.status };
case 'SET_CHART_DATA':
return { ...state, chartData: action.chartData };
default:
return state;
}
}
const CoursePopupGradeDistribution: React.FC<CoursePopupGradeDistributionProps> = ({ course }) => {
const [state, dispatch] = React.useReducer(reducer, initialState);
const ref = React.useRef<HighchartsReact.RefObject>(null);
React.useEffect(() => {
const fetchInitialData = async () => {
try {
const [aggregateDist, semesters] = await queryAggregateDistribution(course);
const initialDistributions: Record<string, Distribution> = { Aggregate: aggregateDist };
const semesterPromises = semesters.map(semester => querySemesterDistribution(course, semester));
const semesterDistributions = await Promise.all(semesterPromises);
semesters.forEach((semester, i) => {
initialDistributions[`${semester.season} ${semester.year}`] = semesterDistributions[i];
});
dispatch({ type: 'SET_DISTRIBUTIONS', distributions: initialDistributions });
dispatch({ type: 'SET_STATUS', status: DataStatus.FOUND });
} catch (e) {
console.error(e);
if (e instanceof NoDataError) {
dispatch({ type: 'SET_STATUS', status: DataStatus.NOT_FOUND });
} else {
dispatch({ type: 'SET_STATUS', status: DataStatus.ERROR });
}
}
};
fetchInitialData();
}, [course]);
React.useEffect(() => {
if (state.status === DataStatus.FOUND && state.distributions[state.semester]) {
const chartData = Object.entries(state.distributions[state.semester]).map(([grade, count]) => ({
y: count,
color: GRADE_COLORS[grade as LetterGrade],
}));
dispatch({ type: 'SET_CHART_DATA', chartData });
}
}, [state.distributions, state.semester, state.status]);
const handleSelectSemester = (event: React.ChangeEvent<HTMLSelectElement>) => {
dispatch({ type: 'SET_SEMESTER', semester: event.target.value });
};
const chartOptions: Highcharts.Options = {
title: { text: undefined },
subtitle: { text: undefined },
legend: { enabled: false },
xAxis: {
title: { text: 'Grade' },
categories: ['A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F'],
crosshair: true,
},
yAxis: {
min: 0,
title: { text: 'Number of Students' },
},
chart: {
style: { fontFamily: 'Roboto Flex', fontWeight: '600' },
spacingBottom: 25,
spacingTop: 25,
height: 250,
},
credits: { enabled: false },
accessibility: { enabled: true },
tooltip: {
headerFormat: '<span style="font-size:small; font-weight:bold">{point.key}</span><table>',
pointFormat:
'<td style="color:{black};padding:0;font-size:small; font-weight:bold;"><b>{point.y:.0f} Students</b></td>',
footerFormat: '</table>',
shared: true,
useHTML: true,
},
plotOptions: {
bar: { pointPadding: 0.2, borderWidth: 0 },
series: { animation: { duration: 700 } },
},
series: [
{
type: 'column',
name: 'Grades',
data: state.chartData,
},
],
};
return (
<div className='pb-[25px] pt-[12px]'>
{state.status === DataStatus.LOADING && <Spinner />}
{state.status === DataStatus.NOT_FOUND && <Text variant='p'>No grade distribution data found</Text>}
{state.status === DataStatus.ERROR && <Text variant='p'>Error fetching grade distribution data</Text>}
{state.status === DataStatus.FOUND && (
<>
<div className='w-full flex items-center justify-center gap-[12px]'>
<Text variant='p'>Grade distribution for {`${course.department} ${course.number}`}</Text>
<select
className='border border rounded-[4px] border-solid px-[12px] py-[8px]'
onChange={handleSelectSemester}
>
{Object.keys(state.distributions)
.sort((k1, k2) => {
if (k1 === 'Aggregate') {
return -1;
}
if (k2 === 'Aggregate') {
return 1;
}
const [season1, year1] = k1.split(' ');
const [, year2] = k2.split(' ');
if (year1 !== year2) {
return parseInt(year2, 10) - parseInt(year1, 10);
}
return season1 === 'Fall' ? 1 : -1;
})
.map(semester => (
<option key={semester} value={semester}>
{semester}
</option>
))}
</select>
</div>
<HighchartsReact ref={ref} highcharts={Highcharts} options={chartOptions} />
</>
)}
</div>
);
};
export default CoursePopupGradeDistribution;