Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show all past enrollment graphs #867

Merged
merged 14 commits into from
Feb 23, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function getColors() {
accumulator[section.sectionCode] = section.color;
return accumulator;
},
{} as { [key: string]: string }
{} as Record<string, string>
);

return courseColors;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,82 @@
import { useState, useEffect, useMemo } from 'react';
import { LineChart, Line, CartesianGrid, ResponsiveContainer, XAxis, YAxis, Tooltip, Legend } from 'recharts';
import { Box, Link, Typography, Skeleton, useMediaQuery } from '@mui/material';
import { useState, useEffect, useMemo, useCallback } from 'react';
import {
LineChart,
Line,
CartesianGrid,
ResponsiveContainer,
XAxis,
YAxis,
Tooltip as RechartsTooltip,
Legend,
} from 'recharts';
import { ArrowBack, ArrowForward } from '@mui/icons-material';
import { Box, IconButton, Link, Typography, Skeleton, Tooltip, useMediaQuery } from '@mui/material';
import { MOBILE_BREAKPOINT } from '../../../globals';
import { DepartmentEnrollmentHistory, EnrollmentHistory } from '$lib/enrollmentHistory';
import { useThemeStore } from '$stores/SettingsStore';

export interface EnrollmentHistoryPopupProps {
type PopupHeaderCallback = () => void;

interface PopupHeaderProps {
graphWidth: number;
graphIndex: number;
handleForward: PopupHeaderCallback;
handleBack: PopupHeaderCallback;
popupTitle: string;
enrollmentHistory: EnrollmentHistory[];
}

function PopupHeader({
graphWidth,
graphIndex,
handleForward,
handleBack,
popupTitle,
enrollmentHistory,
}: PopupHeaderProps) {
const isMobileScreen = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT})`);

return (
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: graphWidth,
}}
>
<Tooltip title="Newer Graph">
{/* In order for a tooltip to work properly with disabled buttons, we need to wrap the button in a span */}
<span>
<IconButton onClick={handleBack} disabled={graphIndex === 0}>
<ArrowBack />
</IconButton>
</span>
</Tooltip>
<Typography sx={{ fontWeight: 500, fontSize: isMobileScreen ? '0.8rem' : '1rem', textAlign: 'center' }}>
{popupTitle}
</Typography>
<Tooltip title="Older Graph">
<span>
<IconButton onClick={handleForward} disabled={graphIndex === enrollmentHistory.length - 1}>
<ArrowForward />
</IconButton>
</span>
</Tooltip>
</Box>
);
}

interface EnrollmentHistoryPopupProps {
department: string;
courseNumber: string;
}

export function EnrollmentHistoryPopup({ department, courseNumber }: EnrollmentHistoryPopupProps) {
const [loading, setLoading] = useState(true);
const [enrollmentHistory, setEnrollmentHistory] = useState<EnrollmentHistory>();
const [enrollmentHistory, setEnrollmentHistory] = useState<EnrollmentHistory[]>();
const [graphIndex, setGraphIndex] = useState(0);

const isMobileScreen = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT})`);

const deptEnrollmentHistory = useMemo(() => new DepartmentEnrollmentHistory(department), [department]);
Expand All @@ -24,15 +88,24 @@ export function EnrollmentHistoryPopup({ department, courseNumber }: EnrollmentH
return 'No past enrollment data found for this course';
}

return `${department} ${courseNumber} | ${enrollmentHistory.year} ${
enrollmentHistory.quarter
} | ${enrollmentHistory.instructors.join(', ')}`;
}, [courseNumber, department, enrollmentHistory]);
const currEnrollmentHistory = enrollmentHistory[graphIndex];
return `${department} ${courseNumber} | ${currEnrollmentHistory.year} ${
currEnrollmentHistory.quarter
} | ${currEnrollmentHistory.instructors.join(', ')}`;
}, [courseNumber, department, enrollmentHistory, graphIndex]);
const isDark = useThemeStore((state) => state.isDark);
const encodedDept = useMemo(() => encodeURIComponent(department), [department]);
const axisColor = isDark ? '#fff' : '#111';
const tooltipDateColor = '#111';

const handleBack = useCallback(() => {
setGraphIndex((prev) => prev - 1);
}, []);

const handleForward = useCallback(() => {
setGraphIndex((prev) => prev + 1);
}, []);

useEffect(() => {
if (!loading) {
return;
Expand All @@ -41,6 +114,7 @@ export function EnrollmentHistoryPopup({ department, courseNumber }: EnrollmentH
deptEnrollmentHistory.find(courseNumber).then((data) => {
if (data) {
setEnrollmentHistory(data);
setGraphIndex(0);
}
setLoading(false);
});
Expand All @@ -64,32 +138,30 @@ export function EnrollmentHistoryPopup({ department, courseNumber }: EnrollmentH
);
}

const lineChartData = enrollmentHistory[graphIndex].days;

return (
<Box sx={{ padding: 0.5 }}>
<Typography
sx={{
marginTop: '.5rem',
textAlign: 'center',
fontWeight: 500,
marginRight: '2rem',
marginLeft: '2rem',
marginBottom: '.5rem',
}}
>
{popupTitle}
</Typography>
<PopupHeader
graphWidth={graphWidth}
graphIndex={graphIndex}
handleForward={handleForward}
handleBack={handleBack}
popupTitle={popupTitle}
enrollmentHistory={enrollmentHistory}
/>
<Link
href={`https://zot-tracker.herokuapp.com/?dept=${encodedDept}&number=${courseNumber}&courseType=all`}
Douglas-Hong marked this conversation as resolved.
Show resolved Hide resolved
target="_blank"
rel="noopener noreferrer"
sx={{ display: 'flex', height: graphHeight, width: graphWidth }}
>
<ResponsiveContainer width="95%" height="95%">
<LineChart data={enrollmentHistory.days} style={{ cursor: 'pointer' }}>
<LineChart data={lineChartData} style={{ cursor: 'pointer' }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" tick={{ fontSize: 12, fill: axisColor }} />
<YAxis tick={{ fontSize: 12, fill: axisColor }} width={40} />
<Tooltip labelStyle={{ color: tooltipDateColor }} />
<RechartsTooltip labelStyle={{ color: tooltipDateColor }} />
<Legend />
<Line type="monotone" dataKey="totalEnrolled" stroke="#8884d8" name="Enrolled" dot={{ r: 2 }} />
<Line type="monotone" dataKey="maxCapacity" stroke="#82ca9d" name="Max" dot={{ r: 2 }} />
Expand Down
25 changes: 10 additions & 15 deletions apps/antalmanac/src/lib/enrollmentHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface EnrollmentHistoryDay {

export class DepartmentEnrollmentHistory {
// Each key in the cache will be the department and courseNumber concatenated
static enrollmentHistoryCache: Record<string, EnrollmentHistory | null> = {};
static enrollmentHistoryCache: Record<string, EnrollmentHistory[] | null> = {};
static termShortNames: string[] = termData.map((term) => term.shortName);
static QUERY_TEMPLATE = `{
enrollmentHistory(department: "$$DEPARTMENT$$", courseNumber: "$$COURSE_NUMBER$$", sectionType: Lec) {
Expand All @@ -68,30 +68,25 @@ export class DepartmentEnrollmentHistory {
this.partialQueryString = DepartmentEnrollmentHistory.QUERY_TEMPLATE.replace('$$DEPARTMENT$$', department);
}

async find(courseNumber: string): Promise<EnrollmentHistory | null> {
async find(courseNumber: string): Promise<EnrollmentHistory[] | null> {
const cacheKey = this.department + courseNumber;
Douglas-Hong marked this conversation as resolved.
Show resolved Hide resolved
return (DepartmentEnrollmentHistory.enrollmentHistoryCache[cacheKey] ??= await this.queryEnrollmentHistory(
courseNumber
));
return (DepartmentEnrollmentHistory.enrollmentHistoryCache[cacheKey] ??=
await this.queryEnrollmentHistory(courseNumber));
}

async queryEnrollmentHistory(courseNumber: string): Promise<EnrollmentHistory | null> {
async queryEnrollmentHistory(courseNumber: string): Promise<EnrollmentHistory[] | null> {
// Query for the enrollment history of all lecture sections that were offered
const queryString = this.partialQueryString.replace('$$COURSE_NUMBER$$', courseNumber);

const res = (await queryGraphQL<EnrollmentHistoryGraphQLResponse>(queryString))?.data?.enrollmentHistory;

if (res?.length) {
const parsedEnrollmentHistory = DepartmentEnrollmentHistory.parseEnrollmentHistoryResponse(res);
DepartmentEnrollmentHistory.sortEnrollmentHistory(parsedEnrollmentHistory);

// For now, just return the enrollment history of the most recent quarter
// instead of the entire array of enrollment histories
const latestEnrollmentHistory = parsedEnrollmentHistory[0];
return latestEnrollmentHistory;
if (!res?.length) {
return null;
}

return null;
const parsedEnrollmentHistory = DepartmentEnrollmentHistory.parseEnrollmentHistoryResponse(res);
DepartmentEnrollmentHistory.sortEnrollmentHistory(parsedEnrollmentHistory);
return parsedEnrollmentHistory;
}

/**
Expand Down
Loading