Skip to content

Commit da8e1cd

Browse files
zaidahmad16AJaccP
andauthored
Feature/core courses toggle 21 (#30)
* Default Explorer to core courses with toggle for full graph The full course graph is overwhelming on first load with heavy edge fan-out. Default to showing only core CS-program courses, with a toggle in the Explorer view to switch to the full course set. Filtering happens outside computeLayout: the visible course set is derived first (core vs all), edges are filtered to those with both endpoints visible, and layout is recomputed over that set so later filter modes can follow the same pattern. * test --------- Co-authored-by: AJaccP <paditya1708@gmail.com>
1 parent 35bfdbd commit da8e1cd

5 files changed

Lines changed: 67 additions & 21 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ package-lock.json
2626

2727
# Output
2828
scripts/output/*.json
29-
coverage/
29+
coverage
30+
coverage/

src/components/ExplorerLegend.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default function ExplorerLegend() {
99
const boxClass = 'w-9 h-4 border-2 self-center rounded';
1010

1111
return (
12-
<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">
12+
<div className="text-sm pointer-events-auto w-full bg-white border border-gray-300 px-3 py-2 rounded-lg shadow-md">
1313
<button
1414
type="button"
1515
onClick={() => setOpen(!open)}

src/components/ExplorerSearch.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export default function ExplorerSearch() {
7777
const open = results.length > 0;
7878

7979
return (
80-
<div className="absolute left-4 top-4 z-10 w-72">
80+
<div className="pointer-events-auto relative z-10 w-full">
8181
<input
8282
ref={inputRef}
8383
type="search"
@@ -102,7 +102,7 @@ export default function ExplorerSearch() {
102102
ref={listRef}
103103
id="explorer-search-results"
104104
role="listbox"
105-
className="mt-1 max-h-64 overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg"
105+
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"
106106
>
107107
{results.map((r, i) => (
108108
<li

src/pages/Explorer.tsx

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ import {
1919
type Edge,
2020
} from '@xyflow/react';
2121
import dagre from '@dagrejs/dagre';
22-
import { courseList, courses, prereqEdges } from '@/data/loadCourses';
22+
import {
23+
courseList,
24+
coreCourseList,
25+
courses,
26+
prereqEdges,
27+
} from '@/data/loadCourses';
2328
import { useExplorerStore } from '@/store/explorerStore';
29+
import type { Course } from '@/types/course';
2430
import CourseNode from '@/components/CourseNode';
2531
import type { CourseNodeData } from '@/components/CourseNode';
2632
import CourseDetailPanel from '@/components/CourseDetailPanel';
@@ -30,22 +36,18 @@ import ExplorerLegend from '@/components/ExplorerLegend';
3036
const NODE_W = 180;
3137
const NODE_H = 60;
3238

33-
// Only draw edges where both endpoints are in our course set; SYSC courses that
34-
// appear in COMP 3004's prereqs are not nodes yet (see TODO #3 above).
35-
const knownCodes = new Set(courseList.map((c) => c.code));
36-
const visibleEdges = prereqEdges.filter(
37-
(e) => knownCodes.has(e.from) && knownCodes.has(e.to),
38-
);
39-
40-
function computeLayout(): {
39+
function computeLayout(
40+
visibleCourses: Course[],
41+
visibleEdges: { from: string; to: string }[],
42+
): {
4143
nodes: Node<CourseNodeData>[];
4244
edges: Edge[];
4345
} {
4446
const g = new dagre.graphlib.Graph();
4547
g.setDefaultEdgeLabel(() => ({}));
4648
g.setGraph({ rankdir: 'TB', ranksep: 80, nodesep: 40 });
4749

48-
for (const course of courseList) {
50+
for (const course of visibleCourses) {
4951
g.setNode(course.code, { width: NODE_W, height: NODE_H });
5052
}
5153

@@ -57,7 +59,7 @@ function computeLayout(): {
5759

5860
dagre.layout(g);
5961

60-
const nodes: Node<CourseNodeData>[] = courseList.map((course) => {
62+
const nodes: Node<CourseNodeData>[] = visibleCourses.map((course) => {
6163
const pos = g.node(course.code);
6264
return {
6365
id: course.code,
@@ -80,11 +82,35 @@ function computeLayout(): {
8082
const nodeTypes: NodeTypes = { courseNode: CourseNode };
8183

8284
export default function Explorer() {
83-
const { selectedCourse, highlightedSet, setSelectedCourse } =
84-
useExplorerStore();
85+
const {
86+
selectedCourse,
87+
highlightedSet,
88+
setSelectedCourse,
89+
showAllCourses,
90+
toggleShowAllCourses,
91+
} = useExplorerStore();
92+
93+
// The single source of truth for "what's shown" — later filter modes
94+
// (reachable-on-click, department/year, search) should derive their own
95+
// visible set the same way, upstream of layout.
96+
const visibleCourses = showAllCourses ? courseList : coreCourseList;
97+
const visibleCodes = useMemo(
98+
() => new Set(visibleCourses.map((c) => c.code)),
99+
[visibleCourses],
100+
);
101+
const visibleEdges = useMemo(
102+
() =>
103+
prereqEdges.filter(
104+
(e) => visibleCodes.has(e.from) && visibleCodes.has(e.to),
105+
),
106+
[visibleCodes],
107+
);
85108

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

89115
// Re-derive edge styles when selection changes.
90116
const edges = useMemo(
@@ -108,9 +134,9 @@ export default function Explorer() {
108134

109135
return (
110136
<div className="relative h-full w-full">
111-
<ExplorerSearch />
112-
<ExplorerLegend />
113137
<ReactFlow
138+
// Remounting on toggle lets React Flow's `fitView` re-fit to the new set.
139+
key={showAllCourses ? 'all' : 'core'}
114140
nodes={nodes}
115141
edges={edges}
116142
nodeTypes={nodeTypes}
@@ -122,6 +148,20 @@ export default function Explorer() {
122148
<Background />
123149
<Controls />
124150
</ReactFlow>
151+
{/* Graph controls stack here so they can't collide; the column ignores
152+
pointer events so canvas panning works through the gaps. */}
153+
<div className="pointer-events-none absolute top-4 left-4 z-10 flex w-72 max-w-[calc(100%-2rem)] flex-col gap-2">
154+
<button
155+
type="button"
156+
onClick={toggleShowAllCourses}
157+
aria-pressed={showAllCourses}
158+
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"
159+
>
160+
{showAllCourses ? 'Show core only' : 'Show all courses'}
161+
</button>
162+
<ExplorerSearch />
163+
<ExplorerLegend />
164+
</div>
125165
<CourseDetailPanel
126166
course={selected}
127167
onClose={() => setSelectedCourse(null)}

src/store/explorerStore.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ interface ExplorerState {
66
selectedCourse: string | null;
77
highlightedSet: Set<string>;
88
setSelectedCourse: (code: string | null) => void;
9+
showAllCourses: boolean;
10+
toggleShowAllCourses: () => void;
911
}
1012

1113
export const useExplorerStore = create<ExplorerState>((set) => ({
@@ -23,4 +25,7 @@ export const useExplorerStore = create<ExplorerState>((set) => ({
2325
...getDescendants(code, prereqEdges),
2426
]),
2527
}),
28+
showAllCourses: false,
29+
toggleShowAllCourses: () =>
30+
set((state) => ({ showAllCourses: !state.showAllCourses })),
2631
}));

0 commit comments

Comments
 (0)