SVG Prerequisite Lines PlannerContext subsystem

Lines connecting course cards are recomputed inside a requestAnimationFrame callback whenever selectedId, placements, showViolLines, substitutions, specialTermPl, or scrollTick changes. Each line is a { from, to, type, fp, tp } object where fp and tp are center-point coordinates in SVG space.

Edge data source
allEdges — computed from courseMap, includes both prereq and coreq edges for all known courses

Coordinate correction

getBoundingClientRect() returns viewport pixels. On desktop the app container has transform: scale(uiScale), so coordinates must be divided by uiScale to convert back to SVG local space. On phone, uiScale is effectively 1 (transform is none), so no correction is applied.

Courses in the "incoming" pseudo-semester are always skipped — no lines are drawn to or from them regardless of mode.

Line types

typeColorConditionMode
prerequisiteGreenA is a prereq of B; A is placed before B — correct orderSelection only
prerequisite-orderRedA is a prereq of B but placed in the same semester or after BBoth
corequisiteBlueA and B are coreqs; both in the same semesterSelection only
corequisite-violRedA and B are coreqs but placed in different semestersBoth
substitution-prereqTealSubstituting course fulfills an inherited prereq — correct orderSelection only
substitution-prereq-orderRedSubstituting course fulfills an inherited prereq but is in the wrong orderBoth

Two drawing modes

The effect runs in one of two modes depending on whether a course is currently selected. Selection mode is a superset of always-on mode — it draws both satisfied and violated lines, while always-on mode draws only red violations.

flowchart TD TICK["Effect triggers\nselectedId / placements / showViolLines\nsubstitutions / specialTermPl / scrollTick"] TICK --> SEL{"selectedId\nset?"} SEL -->|"Yes — Selection mode"| SM["Draw ALL edge types\nfor every edge touching selected course"] SM --> SMTYPES["prerequisite (green)\nprerequisite-order (red)\ncorequisite (blue)\ncorequisite-viol (red)\nsubstitution-prereq (teal)\nsubstitution-prereq-order (red)"] SM --> SUBSEL["Also draw substitution-inherited lines\nwhere selected course is an endpoint"] SEL -->|"No — Always-on mode"| VL{"showViolLines?"} VL -->|false| EMPTY["setLines([])"] VL -->|true| SCAN["Scan ALL placed-course edges"] SCAN --> PQ{"prereq edge?"} PQ -->|Yes| EVTREE{"evalPrereqTree\nreturns 'order'?"} EVTREE -->|Yes| RPREQ["Draw prerequisite-order (red)"] EVTREE -->|No| SKP["skip"] PQ -->|"No — coreq edge"| CSEM{"both in same\nsemester?"} CSEM -->|No| RCOREQ["Draw corequisite-viol (red)"] CSEM -->|Yes| SKP2["skip"] SCAN --> SUBVIOL["Also draw substitution-inherited\nviolation lines (wrong-order only)\nskip if already drawn by selection mode"] SMTYPES & SUBSEL & RPREQ & RCOREQ & SUBVIOL --> SET["setLines(newLines)"]
Always-on mode checks evalPrereqTree per edge to determine if a "wrong order" violation exists. Selection mode skips this check and draws all edge types unconditionally.

scrollTick

Because card positions change when the user scrolls the planner grid, SVG coordinates go stale. A scroll listener on the planner container increments scrollTick by 1. This value is a dependency of the lines effect, so the requestAnimationFrame fires again after scroll and recalculates fresh bounding rects for all visible cards.