+
Rating (Teaching):
{ratingTeaching.toFixed(3)}
+
Rating (Workload):
{ratingWorkload.toFixed(3)}
+
Rating (Content):
{ratingContent.toFixed(3)}
+
Rating (Grading):
{ratingGrading.toFixed(3)}
- Rating (Thumbs Up):
+
+ Rating (Instructor):
{ratingInstructor.toFixed(3)}
+
Overall Rating:
{score.toFixed(3)}
+
+ Confidence:
+ {confidence.toFixed(3)}
+
Percentile:
{(percentile * 100).toFixed(1)}%
-
- Courses{" "}
-
- (A = Available this Semester)
-
-
+
Courses
- {historicalCourses.map((it) => (
-
+ {courses.map((it) => (
+
))}
-
-
-
-
-
- Rating (Teaching):
- {ratingTeaching.toFixed(3)}
- Rating (Thumbs Up):
-
- {ratingInstructor.toFixed(3)}
-
- Overall Rating:
- {score.toFixed(3)}
- Percentile:
-
- {(percentile * 100).toFixed(1)}%
-
-
-
-
Courses
-
- (A = Available this Semester)
-
+
Historical Courses
- {historicalCourses.map((it) => (
-
- ))}
+ {historicalCourses.map(
+ (it) =>
+ courses.map(formatCourse).includes(formatCourse(it)) || (
+
+ ),
+ )}
-
- Instructor Details
-
diff --git a/app/instructor-course-link.tsx b/app/instructor-course-link.tsx
new file mode 100644
index 0000000..944e17e
--- /dev/null
+++ b/app/instructor-course-link.tsx
@@ -0,0 +1,23 @@
+import { type InstructorCourseObject } from "@/data/instructor";
+import { stopPropagation } from "@/lib/events";
+
+export function InstructorCourseLink({
+ course,
+}: {
+ course: InstructorCourseObject;
+}) {
+ const linkUstSpace = `https://ust.space/review/${course.subject}${course.number}`;
+
+ return (
+
+
+ {course.subject}
+ {course.number}
+
+
+ );
+}
diff --git a/app/instructor-trend-chart.css b/app/instructor-trend-chart.css
new file mode 100644
index 0000000..3aac149
--- /dev/null
+++ b/app/instructor-trend-chart.css
@@ -0,0 +1,4 @@
+.recharts-surface {
+ /* Prevent X-Labels from Overflowing */
+ overflow: visible;
+}
diff --git a/components/component/instructor-trend-chart.tsx b/app/instructor-trend-chart.tsx
similarity index 98%
rename from components/component/instructor-trend-chart.tsx
rename to app/instructor-trend-chart.tsx
index f24f57d..4507c5f 100644
--- a/components/component/instructor-trend-chart.tsx
+++ b/app/instructor-trend-chart.tsx
@@ -1,6 +1,6 @@
import "./instructor-trend-chart.css";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
-import { type InstructorScoreObject } from "@/data";
+import { type InstructorScoreObject } from "@/data/instructor";
import { stopPropagation } from "@/lib/events";
import { AreaChart, type CustomTooltipProps } from "@tremor/react";
import ChartTooltip from "@tremor/react/dist/components/chart-elements/common/ChartTooltip";
diff --git a/app/layout.tsx b/app/layout.tsx
index 5da66cc..3b7fd9c 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -36,17 +36,29 @@ export default function RootLayout({
-
+
- FAQ
+ Instructor Rankings
+
+ Course Rankings
+
+ {/* */}
+ {/* FAQ*/}
+ {/**/}
diff --git a/app/page.tsx b/app/page.tsx
index 703995c..16410f8 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,16 +1,15 @@
"use client";
-import { InstructorCard } from "@/components/component/instructor-card";
+import { InstructorCard } from "@/app/instructor-card";
import { NewDomainBanner } from "@/components/component/new-domain-banner";
import { Input } from "@/components/ui/input";
-import { search, type SortBy } from "@/data";
+import { search, type SortBy } from "@/data/instructor";
import dynamic from "next/dynamic";
import React, { type ChangeEvent, useState } from "react";
import { WindowVirtualizer } from "virtua";
const SettingsCard = dynamic(
- async () =>
- (await import("@/components/component/settings-card")).SettingsCard,
+ async () => (await import("@/app/settings-card")).SettingsCard,
{ ssr: false },
);
diff --git a/components/component/settings-card.tsx b/app/settings-card.tsx
similarity index 97%
rename from components/component/settings-card.tsx
rename to app/settings-card.tsx
index d35fb83..a5c46ab 100644
--- a/components/component/settings-card.tsx
+++ b/app/settings-card.tsx
@@ -14,10 +14,10 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
-import { type RatingType, type SortBy } from "@/data";
+import { type RatingType, type SortBy } from "@/data/instructor";
import { stopPropagation } from "@/lib/events";
import { QuestionMarkCircledIcon } from "@radix-ui/react-icons";
-import { type ChangeEvent, forwardRef, useState } from "react";
+import { type ChangeEvent, forwardRef } from "react";
export type SettingsCardProps = {
sortBy: SortBy;
@@ -40,9 +40,7 @@ const NumInput = forwardRef((props, ref) => (
NumInput.displayName = "NumInput";
export function SettingsCard(props: SettingsCardProps) {
- const [openSortBy, setOpenSortBy] = useState(false);
const { sortBy, setSortBy } = props;
- const [openFormula, setOpenFormula] = useState(false);
const { formula, setFormula } = props;
return (
diff --git a/components/component/instructor-course-link.tsx b/components/component/instructor-course-link.tsx
deleted file mode 100644
index e92f518..0000000
--- a/components/component/instructor-course-link.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { type InstructorCourseObject } from "@/data";
-
-export function InstructorCourseLink({
- course,
- thisSem,
-}: {
- course: InstructorCourseObject;
- thisSem: boolean;
-}) {
- const linkUstSpace = `https://ust.space/review/${course.subject}${course.number}`;
- const linkClassScheduleQuota = `https://w5.ab.ust.hk/wcq/cgi-bin/2410/subject/${course.subject}#${course.subject}${course.number}`;
-
- return (
- {
- e.stopPropagation();
- }}
- >
-
- {course.subject}
- {course.number}
-
-
- {thisSem && (
-
- (A)
-
- )}
-
- );
-}
diff --git a/data/course.ts b/data/course.ts
new file mode 100644
index 0000000..a9a7e62
--- /dev/null
+++ b/data/course.ts
@@ -0,0 +1,134 @@
+import dataCourseJSON from "./data-course.json";
+import Fuse from "fuse.js";
+import _ from "lodash";
+
+export type CourseScoreObject = {
+ subject: string;
+ number: string;
+ term: number;
+
+ ratingContent: number;
+ ratingTeaching: number;
+ ratingGrading: number;
+ ratingWorkload: number;
+ samples: number;
+ confidence: number;
+
+ individualRatingContent: number;
+ individualRatingTeaching: number;
+ individualRatingGrading: number;
+ individualRatingWorkload: number;
+ individualSamples: number;
+ individualConfidence: number;
+
+ bayesianRatingContent: number;
+ bayesianRatingTeaching: number;
+ bayesianRatingGrading: number;
+ bayesianRatingWorkload: number;
+
+ score: number;
+ bayesianScore: number;
+};
+
+export type CourseExtraObject = {
+ subject: string;
+ number: string;
+ terms: number[];
+ instructors: string[];
+ historicalInstructors: string[];
+};
+
+export type CourseObject = {
+ rank: number;
+ percentile: number;
+ scores: CourseScoreObject[];
+} & CourseExtraObject;
+
+export const dataCourseObjects = dataCourseJSON as Record;
+export const dataCourseKeys = Object.keys(dataCourseObjects);
+
+export type CourseSortBy = keyof Pick<
+ CourseScoreObject,
+ | "bayesianRatingContent"
+ | "bayesianRatingTeaching"
+ | "bayesianRatingGrading"
+ | "bayesianRatingWorkload"
+ | "ratingContent"
+ | "ratingTeaching"
+ | "ratingGrading"
+ | "ratingWorkload"
+ | "score"
+ | "bayesianScore"
+>;
+
+export type CourseRatings = keyof Pick<
+ CourseScoreObject,
+ "ratingContent" | "ratingTeaching" | "ratingGrading" | "ratingWorkload"
+>;
+
+export type CourseRatingWeights = Record;
+
+export function searchCourses(
+ query: string,
+ sortBy: CourseSortBy,
+ weights: CourseRatingWeights,
+): CourseObject[] {
+ const sortedInstructorObjects = _.chain(dataCourseObjects)
+ .values()
+ // Calculate the score by the given formula
+ .map((courseObj) => ({
+ ...courseObj,
+ scores: courseObj.scores.map((score) => ({
+ ...score,
+ score:
+ weights.ratingContent * score.ratingContent +
+ weights.ratingTeaching * score.ratingTeaching +
+ weights.ratingGrading * score.ratingGrading +
+ weights.ratingWorkload * score.ratingWorkload,
+ bayesianScore:
+ weights.ratingContent * score.bayesianRatingContent +
+ weights.ratingTeaching * score.bayesianRatingTeaching +
+ weights.ratingGrading * score.bayesianRatingGrading +
+ weights.ratingWorkload * score.bayesianRatingWorkload,
+ })),
+ }))
+ // Sort the courses by the given criterion
+ .sortBy((instructor) => -instructor.scores[0][sortBy])
+ // Assign the rank and percentile to each course
+ .map((instructorObj, i) => ({
+ ...instructorObj,
+ rank: i + 1,
+ percentile: 1 - i / dataCourseKeys.length,
+ }))
+ // Create searching indices
+ .map((courseObj) => ({
+ subject: courseObj.subject,
+ number: courseObj.number,
+ course: `${courseObj.subject} ${courseObj.number}`,
+ instructors: [
+ ...courseObj.instructors.map((instructor) => `${instructor} (A)`),
+ ...courseObj.historicalInstructors.map((instructor) => `${instructor}`),
+ ...courseObj.instructors.flatMap((instructor) =>
+ instructor.split(", "),
+ ),
+ ...courseObj.historicalInstructors.flatMap((instructor) =>
+ instructor.split(", "),
+ ),
+ ],
+ obj: courseObj,
+ }))
+ .value();
+
+ if (query) {
+ const fuse = new Fuse(sortedInstructorObjects, {
+ keys: ["subject", "number", "course", "instructors"],
+ shouldSort: false,
+ useExtendedSearch: true,
+ threshold: 0.1,
+ });
+
+ return fuse.search(query).map((it) => it.item.obj);
+ }
+
+ return sortedInstructorObjects.map((it) => it.obj);
+}
diff --git a/data/index.ts b/data/instructor.ts
similarity index 100%
rename from data/index.ts
rename to data/instructor.ts
diff --git a/package.json b/package.json
index d0fe014..c3ed93a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ust-rankings",
- "version": "1.0.0",
+ "version": "1.1.0",
"private": true,
"scripts": {
"update-data": "node scripts/update-data.mjs",
diff --git a/scripts/update-data.mjs b/scripts/update-data.mjs
index 77203ca..c576823 100644
--- a/scripts/update-data.mjs
+++ b/scripts/update-data.mjs
@@ -15,4 +15,13 @@ async function updateInstructorData() {
);
}
-await Promise.all([updateInstructorData()]);
+async function updateCourseData() {
+ await fs.writeFile(
+ "data/data-course.json",
+ await fetchText(
+ "https://raw.githubusercontent.com/ust-archive/ust-rankings-data/main/data-course.json",
+ ),
+ );
+}
+
+await Promise.all([updateInstructorData(), updateCourseData()]);