Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ dist-ssr
*.sw?

# Output
scripts/output/*.json
scripts/output/*.json
coverage
65 changes: 50 additions & 15 deletions src/pages/Explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,33 @@ import {
type Edge,
} from '@xyflow/react';
import dagre from '@dagrejs/dagre';
import { courseList, courses, prereqEdges } from '@/data/loadCourses';
import {
courseList,
coreCourseList,
courses,
prereqEdges,
} from '@/data/loadCourses';
import { useExplorerStore } from '@/store/explorerStore';
import type { Course } from '@/types/course';
import CourseNode from '@/components/CourseNode';
import type { CourseNodeData } from '@/components/CourseNode';
import CourseDetailPanel from '@/components/CourseDetailPanel';

const NODE_W = 180;
const NODE_H = 60;

// Only draw edges where both endpoints are in our course set; SYSC courses that
// appear in COMP 3004's prereqs are not nodes yet (see TODO #3 above).
const knownCodes = new Set(courseList.map((c) => c.code));
const visibleEdges = prereqEdges.filter(
(e) => knownCodes.has(e.from) && knownCodes.has(e.to),
);

function computeLayout(): {
function computeLayout(
visibleCourses: Course[],
visibleEdges: { from: string; to: string }[],
): {
nodes: Node<CourseNodeData>[];
edges: Edge[];
} {
const g = new dagre.graphlib.Graph();
g.setDefaultEdgeLabel(() => ({}));
g.setGraph({ rankdir: 'TB', ranksep: 80, nodesep: 40 });

for (const course of courseList) {
for (const course of visibleCourses) {
g.setNode(course.code, { width: NODE_W, height: NODE_H });
}

Expand All @@ -55,7 +57,7 @@ function computeLayout(): {

dagre.layout(g);

const nodes: Node<CourseNodeData>[] = courseList.map((course) => {
const nodes: Node<CourseNodeData>[] = visibleCourses.map((course) => {
const pos = g.node(course.code);
return {
id: course.code,
Expand All @@ -78,11 +80,35 @@ function computeLayout(): {
const nodeTypes: NodeTypes = { courseNode: CourseNode };

export default function Explorer() {
const { selectedCourse, highlightedSet, setSelectedCourse } =
useExplorerStore();
const {
selectedCourse,
highlightedSet,
setSelectedCourse,
showAllCourses,
toggleShowAllCourse,
} = useExplorerStore();

// The single source of truth for "what's shown" — later filter modes
// (reachable-on-click, department/year, search) should derive their own
// visible set the same way, upstream of layout.
const visibleCourses = showAllCourses ? courseList : coreCourseList;
const visibleCodes = useMemo(
() => new Set(visibleCourses.map((c) => c.code)),
[visibleCourses],
);
const visibleEdges = useMemo(
() =>
prereqEdges.filter(
(e) => visibleCodes.has(e.from) && visibleCodes.has(e.to),
),
[visibleCodes],
);

// Layout is derived entirely from static import-time data; deps array is empty.
const { nodes, edges: layoutEdges } = useMemo(() => computeLayout(), []);
// Layout is recomputed whenever the visible course set changes.
const { nodes, edges: layoutEdges } = useMemo(
() => computeLayout(visibleCourses, visibleEdges),
[visibleCourses, visibleEdges],
);

// Re-derive edge styles when selection changes.
const edges = useMemo(
Expand All @@ -107,6 +133,8 @@ export default function Explorer() {
return (
<div className="relative h-full w-full">
<ReactFlow
// Remounting on toggle lets React Flow's `fitView` re-fit to the new set.
key={showAllCourses ? 'all' : 'core'}
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
Expand All @@ -118,6 +146,13 @@ export default function Explorer() {
<Background />
<Controls />
</ReactFlow>
<button
type="button"
onClick={toggleShowAllCourse}
className="absolute top-4 left-4 z-10 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50"
>
{showAllCourses ? 'Show core only' : 'Show all courses'}
</button>
<CourseDetailPanel
course={selected}
onClose={() => setSelectedCourse(null)}
Expand Down
5 changes: 5 additions & 0 deletions src/store/explorerStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ interface ExplorerState {
selectedCourse: string | null;
highlightedSet: Set<string>;
setSelectedCourse: (code: string | null) => void;
showAllCourses: boolean;
toggleShowAllCourse: () => void;
}

export const useExplorerStore = create<ExplorerState>((set) => ({
Expand All @@ -23,4 +25,7 @@ export const useExplorerStore = create<ExplorerState>((set) => ({
...getDescendants(code, prereqEdges),
]),
}),
showAllCourses: false,
toggleShowAllCourse: () =>
set((state) => ({ showAllCourses: !state.showAllCourses })),
}));