Skip to content
Merged
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 @@ -26,4 +26,5 @@ package-lock.json

# Output
scripts/output/*.json
coverage/
coverage
coverage/
2 changes: 1 addition & 1 deletion src/components/ExplorerLegend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function ExplorerLegend() {
const boxClass = 'w-9 h-4 border-2 self-center rounded';

return (
<div className="text-sm absolute left-4 top-16 w-72 bg-white border border-gray-300 px-3 py-2 rounded-lg shadow-md z-1">
<div className="text-sm pointer-events-auto w-full bg-white border border-gray-300 px-3 py-2 rounded-lg shadow-md">
<button
type="button"
onClick={() => setOpen(!open)}
Expand Down
4 changes: 2 additions & 2 deletions src/components/ExplorerSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default function ExplorerSearch() {
const open = results.length > 0;

return (
<div className="absolute left-4 top-4 z-10 w-72">
<div className="pointer-events-auto relative z-10 w-full">
<input
ref={inputRef}
type="search"
Expand All @@ -102,7 +102,7 @@ export default function ExplorerSearch() {
ref={listRef}
id="explorer-search-results"
role="listbox"
className="mt-1 max-h-64 overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg"
className="absolute top-full left-0 mt-1 max-h-64 w-full overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg"
>
{results.map((r, i) => (
<li
Expand Down
74 changes: 57 additions & 17 deletions src/pages/Explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ 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';
Expand All @@ -30,22 +36,18 @@ import ExplorerLegend from '@/components/ExplorerLegend';
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 @@ -57,7 +59,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 @@ -80,11 +82,35 @@ function computeLayout(): {
const nodeTypes: NodeTypes = { courseNode: CourseNode };

export default function Explorer() {
const { selectedCourse, highlightedSet, setSelectedCourse } =
useExplorerStore();
const {
selectedCourse,
highlightedSet,
setSelectedCourse,
showAllCourses,
toggleShowAllCourses,
} = 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 @@ -108,9 +134,9 @@ export default function Explorer() {

return (
<div className="relative h-full w-full">
<ExplorerSearch />
<ExplorerLegend />
<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 @@ -122,6 +148,20 @@ export default function Explorer() {
<Background />
<Controls />
</ReactFlow>
{/* Graph controls stack here so they can't collide; the column ignores
pointer events so canvas panning works through the gaps. */}
<div className="pointer-events-none absolute top-4 left-4 z-10 flex w-72 max-w-[calc(100%-2rem)] flex-col gap-2">
<button
type="button"
onClick={toggleShowAllCourses}
aria-pressed={showAllCourses}
className="pointer-events-auto self-start 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>
<ExplorerSearch />
<ExplorerLegend />
</div>
<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;
toggleShowAllCourses: () => void;
}

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