diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 1248a8f..e8ffe22 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,8 +1,3 @@ { - "servers": { - "nx-mcp": { - "type": "sse", - "url": "http://localhost:9980/sse" - } - } + "servers": {} } \ No newline at end of file diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 7a4b155..6a90add 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -2,7 +2,7 @@ import morgan from "morgan"; import express, { ErrorRequestHandler } from "express"; import cors from "cors"; import { isUser } from "~/controllers/user"; -import { getAllCourses, getCourseByID, getCourses, getFilteredCourses, getRequisites } from "~/controllers/courses"; +import { getAllCourses, getCourseByID, getCourses, getFilteredCourses, getRequisites, getRequisitesGraph } from "~/controllers/courses"; import { getFCEs } from "~/controllers/fces"; import { getInstructors } from "~/controllers/instructors"; import { getGeneds } from "~/controllers/geneds"; @@ -24,6 +24,10 @@ app.route("/courses/all").get(getAllCourses); app.route("/courses/requisites/:courseID").get(getRequisites); app.route("/courses/search/").get(getFilteredCourses); app.route("/courses/search/").post(isUser, getFilteredCourses); +// app.route("/courses/requisites-graph").get(getRequisitesGraph); +console.log("Registering /courses/requisites-graph"); +app.get("/courses/requisites-graph", getRequisitesGraph); + app.route("/fces").post(isUser, getFCEs); diff --git a/apps/backend/src/controllers/courses.ts b/apps/backend/src/controllers/courses.ts index fa2e293..88723f4 100644 --- a/apps/backend/src/controllers/courses.ts +++ b/apps/backend/src/controllers/courses.ts @@ -321,3 +321,54 @@ export const getRequisites: RequestHandler = async (req, res, next) => { next(e); } }; + +// --- Full Requisites Graph Endpoint (DAG) --- +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const getRequisitesGraph: RequestHandler = async (_req, res, next) => { + try { + const courses = await db.courses.findMany({ + select: { + courseID: true, + name: true, + department: true, + units: true, + prereqs: true, + }, + }); + + const nodes: Record< + string, + { + courseID: string; + name: string; + department: string; + units: string | number | null; + } + > = {}; + + const edges: { source: string; target: string; kind: "prereq" }[] = []; + + for (const c of courses) { + // node for each course + nodes[c.courseID] = { + courseID: c.courseID, + name: c.name, + department: c.department, + units: c.units, + }; + + // edges for each prereq + for (const prereq of c.prereqs ?? []) { + edges.push({ + source: prereq, + target: c.courseID, + kind: "prereq", + }); + } + } + + res.json({ nodes, edges }); + } catch (err) { + next(err); + } +}; diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 280671d..cd2f6c5 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -31,6 +31,7 @@ "@types/uuid": "^10.0.0", "@yornaath/batshit": "^0.10.1", "axios": "^1.11.0", + "dagre": "^0.8.5", "downshift": "^9.0.10", "fuse.js": "^7.1.0", "ical.js": "^2.2.1", @@ -50,6 +51,7 @@ "react-spinners": "^0.17.0", "react-string-replace": "^1.1.1", "react-tooltip": "^5.29.1", + "reactflow": "^11.11.4", "redux-persist": "^6.0.0", "use-deep-compare-effect": "^1.8.1", "uuid": "^11.1.0" diff --git a/apps/frontend/src/app/store.ts b/apps/frontend/src/app/store.ts index 2dbc272..3501484 100644 --- a/apps/frontend/src/app/store.ts +++ b/apps/frontend/src/app/store.ts @@ -65,7 +65,7 @@ const reducers = combineReducers({ version: 1, storage, stateReconciler: autoMergeLevel2, - blacklist: ["session"], + blacklist: ["session", "searchBarMounted"], }, uiReducer ), diff --git a/apps/frontend/src/app/ui.ts b/apps/frontend/src/app/ui.ts index f9cd6a6..3b530e3 100644 --- a/apps/frontend/src/app/ui.ts +++ b/apps/frontend/src/app/ui.ts @@ -4,12 +4,15 @@ export interface UIState { darkMode: boolean; sidebarOpen: boolean; schedulesTopbarOpen: boolean; + // Guards against rendering multiple course search bars simultaneously. + searchBarMounted: boolean; } const initialState: UIState = { darkMode: false, sidebarOpen: true, schedulesTopbarOpen: false, + searchBarMounted: false, }; export const uiSlice = createSlice({ @@ -25,6 +28,9 @@ export const uiSlice = createSlice({ toggleSchedulesTopbarOpen: (state) => { state.schedulesTopbarOpen = !state.schedulesTopbarOpen; }, + setSearchBarMounted: (state, action) => { + state.searchBarMounted = action.payload as boolean; + }, }, }); diff --git a/apps/frontend/src/components/SearchBar.tsx b/apps/frontend/src/components/SearchBar.tsx index 7356ef7..0a57788 100644 --- a/apps/frontend/src/components/SearchBar.tsx +++ b/apps/frontend/src/components/SearchBar.tsx @@ -13,6 +13,7 @@ import { filtersSlice } from "~/app/filters"; import { getPillboxes } from "./filters/LevelFilter"; import { useFetchCourseInfosByPage } from "~/app/api/course"; import { useAuth } from "@clerk/nextjs"; +import { uiSlice } from "~/app/ui"; const AppliedFiltersPill = ({ className, @@ -129,6 +130,24 @@ const AppliedFilters = () => { const SearchBar = () => { const dispatch = useAppDispatch(); + const searchBarMounted = useAppSelector((state) => state.ui.searchBarMounted); + const [hasClaimedSlot, setHasClaimedSlot] = useState(false); + + useEffect(() => { + // Ensure only one search bar can render at a time by claiming a global slot. + if (!hasClaimedSlot && !searchBarMounted) { + setHasClaimedSlot(true); + dispatch(uiSlice.actions.setSearchBarMounted(true)); + } + + return () => { + if (hasClaimedSlot) { + dispatch(uiSlice.actions.setSearchBarMounted(false)); + } + }; + }, [dispatch, hasClaimedSlot, searchBarMounted]); + + if (!hasClaimedSlot && searchBarMounted) return null; const initialSearch = useAppSelector((state) => state.filters.search); const [search, setSearch] = useState(initialSearch); diff --git a/apps/frontend/src/layoutGraph.ts b/apps/frontend/src/layoutGraph.ts new file mode 100644 index 0000000..b3cd645 --- /dev/null +++ b/apps/frontend/src/layoutGraph.ts @@ -0,0 +1,38 @@ +import * as dagre from "dagre"; +import type { Node, Edge } from "reactflow"; + +const NODE_WIDTH = 180; +const NODE_HEIGHT = 60; + +export function layoutDAG( + nodes: Node[], + edges: Edge[], + direction: "LR" | "TB" = "LR" +) { + const g = new dagre.graphlib.Graph({ directed: true }); + + g.setDefaultEdgeLabel(() => ({})); + g.setGraph({ rankdir: direction }); + + nodes.forEach((n) => { + g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT }); + }); + + edges.forEach((e) => g.setEdge(e.source, e.target)); + + dagre.layout(g); + + return { + nodes: nodes.map((n) => { + const pos = g.node(n.id); + return { + ...n, + position: { + x: pos.x - NODE_WIDTH / 2, + y: pos.y - NODE_HEIGHT / 2, + }, + }; + }), + edges, + }; +} diff --git a/apps/frontend/src/pages/index.tsx b/apps/frontend/src/pages/index.tsx index 93c3fef..e1517db 100644 --- a/apps/frontend/src/pages/index.tsx +++ b/apps/frontend/src/pages/index.tsx @@ -1,4 +1,5 @@ import type { NextPage } from "next"; +import Link from "next/link"; import Filter from "~/components/Filter"; import Aggregate from "~/components/Aggregate"; import Topbar from "~/components/Topbar"; @@ -7,6 +8,7 @@ import CourseSearchList from "~/components/CourseSearchList"; import React from "react"; import { Page } from "~/components/Page"; + const IndexPage: NextPage = () => { return ( { content={ <> - +
+
+ +
+ + + Course Requisites Graph + +
+ } @@ -29,4 +43,5 @@ const IndexPage: NextPage = () => { ); }; + export default IndexPage; diff --git a/apps/frontend/src/pages/requisites.tsx b/apps/frontend/src/pages/requisites.tsx new file mode 100644 index 0000000..b1673b6 --- /dev/null +++ b/apps/frontend/src/pages/requisites.tsx @@ -0,0 +1,357 @@ +import type { NextPage } from "next"; +import React, { useEffect, useMemo, useState } from "react"; +import ReactFlow, { + Background, + Controls, + MiniMap, + Node, + Edge, + MarkerType, +} from "reactflow"; +import "reactflow/dist/style.css"; + +import axios from "axios"; +import { Page } from "~/components/Page"; + +type BackendCourseNode = { + courseID: string; + name: string; + department: string; + units: number | string | null; +}; + +type BackendEdge = { + source: string; + target: string; + kind: "prereq"; +}; + +type GraphResponse = { + nodes: Record; + edges: BackendEdge[]; +}; + +// πŸ”₯ Bold, high-contrast colors per department +const departmentColor = (course: BackendCourseNode): string => { + const dept = + course.department?.toLowerCase() ?? + course.courseID.slice(0, 2).toLowerCase(); + + if (dept.startsWith("15") || dept.includes("cs")) return "#0033FF"; // CS + if (dept.startsWith("21") || dept.includes("math")) return "#00A650"; // Math + if (dept.startsWith("17") || dept.includes("se")) return "#9B00FF"; // SE + if (dept.startsWith("05") || dept.includes("hcii")) return "#FF6A00"; // HCII + if (dept.startsWith("36") || dept.includes("stat")) return "#009F9F"; // Stats + if (dept.startsWith("84") || dept.includes("poli")) return "#FF0033"; // Policy + if (dept.startsWith("04")) return "#4D58FF"; // IS + + return "#444444"; // fallback for other departments +}; + +const RequisitesPage: NextPage = () => { + const [rawNodes, setRawNodes] = useState>( + {} + ); + const [rawEdges, setRawEdges] = useState([]); + const [loading, setLoading] = useState(true); + + // ───────────────────────────── load graph from backend ───────────────────────── + useEffect(() => { + const load = async () => { + try { + const base = process.env.NEXT_PUBLIC_BACKEND_URL ?? ""; + const url = `${base}/courses/requisites-graph`; + + const res = await axios.get(url); + + setRawNodes(res.data.nodes || {}); + setRawEdges(res.data.edges || []); + } catch (err) { + console.error("Error fetching requisites graph:", err); + } finally { + setLoading(false); + } + }; + + void load(); + }, []); + + const coursesArray = useMemo( + () => Object.values(rawNodes), + [rawNodes] + ); + + const hasData = coursesArray.length > 0; + + // ───────────────────────────── compute layout + styling ──────────────────────── + const { nodes, edges } = useMemo(() => { + if (!hasData) { + return { nodes: [] as Node[], edges: [] as Edge[] }; + } + + // Only keep edges whose endpoints actually exist as nodes + const validEdges = rawEdges.filter( + (e) => rawNodes[e.source] && rawNodes[e.target] + ); + + // Collect all node IDs we care about + const ids = Object.keys(rawNodes); + + // Graph structures + const indegree = new Map(); + const level = new Map(); + const outgoing = new Map(); + + // Initialize maps for all known ids + ids.forEach((id) => { + indegree.set(id, 0); + level.set(id, 0); + outgoing.set(id, []); + }); + + // Build indegree + outgoing safely + validEdges.forEach((e) => { + // Ensure source exists in maps + if (!outgoing.has(e.source)) { + outgoing.set(e.source, []); + } + if (!indegree.has(e.source)) { + indegree.set(e.source, 0); + level.set(e.source, 0); + } + + // Ensure target exists in maps + if (!outgoing.has(e.target)) { + outgoing.set(e.target, []); + } + if (!indegree.has(e.target)) { + indegree.set(e.target, 0); + level.set(e.target, 0); + } + + // Update indegree + outgoing + indegree.set(e.target, (indegree.get(e.target) ?? 0) + 1); + outgoing.get(e.source)!.push(e.target); + }); + + // Kahn's algorithm to assign levels (columns) + const queue: string[] = []; + indegree.forEach((deg, id) => { + if (deg === 0) queue.push(id); + }); + + while (queue.length > 0) { + const u = queue.shift()!; + const uLevel = level.get(u) ?? 0; + + for (const v of outgoing.get(u) ?? []) { + const current = level.get(v) ?? 0; + if (uLevel + 1 > current) { + level.set(v, uLevel + 1); + } + + const newDeg = (indegree.get(v) ?? 0) - 1; + indegree.set(v, newDeg); + if (newDeg === 0) queue.push(v); + } + } + + // Group node ids by level + const levels: Record = {}; + Array.from(level.keys()).forEach((id) => { + if (!rawNodes[id]) return; // ignore phantom ids + const l = level.get(id) ?? 0; + if (!levels[l]) levels[l] = []; + levels[l].push(id); + }); + + const sortedLevels = Object.keys(levels) + .map((k) => parseInt(k, 10)) + .sort((a, b) => a - b); + + const xStep = 260; + const yStep = 120; + + const nodeMap: Node[] = []; + + // Create positioned nodes + sortedLevels.forEach((lvl) => { + const colIds = levels[lvl]; + const count = colIds.length; + + colIds.forEach((id, index) => { + const course = rawNodes[id]; + if (!course) return; + + const color = departmentColor(course); + + const x = lvl * xStep; + const y = (index - (count - 1) / 2) * yStep; + + nodeMap.push({ + id: course.courseID, + position: { x, y }, + data: { + label: ( +
+
+ {course.courseID} +
+
+ {course.name} +
+
+ {course.department} +
+
+ ), + }, + style: { + borderRadius: 8, + padding: 6, + border: `2.5px solid ${color}`, + backgroundColor: "#ffffff", + }, + }); + }); + }); + + // Create edges with arrowheads + const edgeMap: Edge[] = validEdges.map((e, idx) => ({ + id: `edge-${idx}`, + source: e.source, + target: e.target, + type: "smoothstep", + style: { + stroke: "#475569", + strokeWidth: 1.6, + }, + markerEnd: { + type: MarkerType.ArrowClosed, + color: "#475569", + }, + })); + + return { nodes: nodeMap, edges: edgeMap }; + }, [hasData, rawNodes, rawEdges]); + + // ───────────────────────────── render ────────────────────────────────────────── + return ( + + {/* Header */} +
+

Course Requisites Graph

+

+ Visual DAG of all CMU course prerequisites and postrequisites. + Arrows point from a prerequisite to the course that depends on it. +

+ + {/* Legend */} +
+
+ + CS / 15-xxx +
+
+ + Math / 21-xxx +
+
+ + SE / 17-xxx +
+
+ + HCII / 05-xxx +
+
+ + Stats / 36-xxx +
+
+ + Policy / 84-xxx +
+
+ + IS / 04-xxx +
+
+ + Other depts +
+
+ + + + + + + + + prereq β†’ dependent +
+
+
+ + {/* Graph */} +
+ {loading ? ( +
+ Loading graph… +
+ ) : !hasData ? ( +
+ No requisites data returned. +
+ ) : ( +
+ + + + + +
+ )} +
+ + } + /> + ); +}; + +export default RequisitesPage; diff --git a/apps/frontend/src/types/dagre.d.ts b/apps/frontend/src/types/dagre.d.ts new file mode 100644 index 0000000..40b2ed9 --- /dev/null +++ b/apps/frontend/src/types/dagre.d.ts @@ -0,0 +1,13 @@ +declare module "dagre" { + export namespace graphlib { + class Graph { + constructor(options?: { directed?: boolean }); + setGraph(graph: { rankdir?: "LR" | "TB" }): void; + setDefaultEdgeLabel(cb: () => any): void; + setNode(id: string, cfg: { width: number; height: number }): void; + setEdge(from: string, to: string): void; + node(id: string): { x: number; y: number }; + } + } + export function layout(g: graphlib.Graph): void; +} diff --git a/bun.lock b/bun.lock index 1815d9f..8ae4056 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "cmucourses", @@ -70,6 +71,7 @@ "@types/uuid": "^10.0.0", "@yornaath/batshit": "^0.10.1", "axios": "^1.11.0", + "dagre": "^0.8.5", "downshift": "^9.0.10", "fuse.js": "^7.1.0", "ical.js": "^2.2.1", @@ -89,6 +91,7 @@ "react-spinners": "^0.17.0", "react-string-replace": "^1.1.1", "react-tooltip": "^5.29.1", + "reactflow": "^11.11.4", "redux-persist": "^6.0.0", "use-deep-compare-effect": "^1.8.1", "uuid": "^11.1.0", @@ -853,6 +856,18 @@ "@react-types/shared": ["@react-types/shared@3.32.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ=="], + "@reactflow/background": ["@reactflow/background@11.3.14", "", { "dependencies": { "@reactflow/core": "11.11.4", "classcat": "^5.0.3", "zustand": "^4.4.1" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA=="], + + "@reactflow/controls": ["@reactflow/controls@11.2.14", "", { "dependencies": { "@reactflow/core": "11.11.4", "classcat": "^5.0.3", "zustand": "^4.4.1" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw=="], + + "@reactflow/core": ["@reactflow/core@11.11.4", "", { "dependencies": { "@types/d3": "^7.4.0", "@types/d3-drag": "^3.0.1", "@types/d3-selection": "^3.0.3", "@types/d3-zoom": "^3.0.1", "classcat": "^5.0.3", "d3-drag": "^3.0.0", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0", "zustand": "^4.4.1" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q=="], + + "@reactflow/minimap": ["@reactflow/minimap@11.7.14", "", { "dependencies": { "@reactflow/core": "11.11.4", "@types/d3-selection": "^3.0.3", "@types/d3-zoom": "^3.0.1", "classcat": "^5.0.3", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0", "zustand": "^4.4.1" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ=="], + + "@reactflow/node-resizer": ["@reactflow/node-resizer@2.2.14", "", { "dependencies": { "@reactflow/core": "11.11.4", "classcat": "^5.0.4", "d3-drag": "^3.0.0", "d3-selection": "^3.0.0", "zustand": "^4.4.1" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA=="], + + "@reactflow/node-toolbar": ["@reactflow/node-toolbar@1.3.14", "", { "dependencies": { "@reactflow/core": "11.11.4", "classcat": "^5.0.3", "zustand": "^4.4.1" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ=="], + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.8.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^10.0.3", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A=="], "@restart/hooks": ["@restart/hooks@0.4.16", "", { "dependencies": { "dequal": "^2.0.3" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w=="], @@ -1027,6 +1042,68 @@ "@types/cors": ["@types/cors@2.8.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA=="], + "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + + "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + + "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + + "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + + "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], + + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + + "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + "@types/date-arithmetic": ["@types/date-arithmetic@4.1.4", "", {}, "sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw=="], "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], @@ -1041,6 +1118,8 @@ "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/hoist-non-react-statics": ["@types/hoist-non-react-statics@3.3.6", "", { "dependencies": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" } }, "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw=="], "@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="], @@ -1423,6 +1502,8 @@ "cjs-module-lexer": ["cjs-module-lexer@2.1.0", "", {}, "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA=="], + "classcat": ["classcat@5.0.5", "", {}, "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="], + "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], @@ -1533,6 +1614,26 @@ "cva": ["cva@1.0.0-beta.4", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "typescript": ">= 4.5.5" }, "optionalPeers": ["typescript"] }, "sha512-F/JS9hScapq4DBVQXcK85l9U91M6ePeXoBMSp7vypzShoefUBxjQTo3g3935PUHgQd+IW77DjbPRIxugy4/GCQ=="], + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + + "dagre": ["dagre@0.8.5", "", { "dependencies": { "graphlib": "^2.1.8", "lodash": "^4.17.15" } }, "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw=="], + "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], @@ -1867,6 +1968,8 @@ "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + "graphlib": ["graphlib@2.1.8", "", { "dependencies": { "lodash": "^4.17.15" } }, "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A=="], + "handle-thing": ["handle-thing@2.0.1", "", {}, "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg=="], "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], @@ -2629,6 +2732,8 @@ "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], + "reactflow": ["reactflow@11.11.4", "", { "dependencies": { "@reactflow/background": "11.3.14", "@reactflow/controls": "11.2.14", "@reactflow/core": "11.11.4", "@reactflow/minimap": "11.7.14", "@reactflow/node-resizer": "2.2.14", "@reactflow/node-toolbar": "1.3.14" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og=="], + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -3075,6 +3180,8 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], + "@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],