/* Mastery map — the learner's relationship to the field Same graph substrate as PrerequisiteGraph but projected through learning state. FSRS retention is first-class. Domain-agnostic structure. */ const MasteryMap = () => { const [hovered, setHovered] = React.useState(null); const [filter, setFilter] = React.useState("all"); // Same calculus subgraph as v1 but with FSRS retention state added. // mastery: mastered | review | learning | frontier | locked // retention: 0..1 (FSRS-projected probability of recall right now) // due: days until/since due (negative = overdue) const nodes = [ // primitives — long mastered, high retention { id:"set", label:"Set", kind:"definition", x:0.08, y:0.10, mastery:"mastered", retention:0.96, due: 18 }, { id:"function", label:"Function", kind:"definition", x:0.20, y:0.08, mastery:"mastered", retention:0.94, due: 14 }, { id:"real_numbers", label:"Real numbers ℝ", kind:"axiom", x:0.32, y:0.14, mastery:"mastered", retention:0.91, due: 12 }, { id:"interval", label:"Open interval", kind:"definition", x:0.44, y:0.06, mastery:"mastered", retention:0.88, due: 9 }, { id:"absolute_val", label:"Absolute value", kind:"definition", x:0.55, y:0.13, mastery:"mastered", retention:0.93, due: 22 }, { id:"composition", label:"Composition", kind:"definition", x:0.68, y:0.08, mastery:"mastered", retention:0.81, due: 4 }, { id:"sequence", label:"Sequence", kind:"definition", x:0.80, y:0.14, mastery:"mastered", retention:0.62, due: -2 }, // overdue { id:"slope_line", label:"Slope of a line", kind:"definition", x:0.92, y:0.08, mastery:"mastered", retention:0.55, due: -5 }, // overdue → review // limits { id:"limit_seq", label:"Limit · sequence", kind:"definition", x:0.10, y:0.30, mastery:"mastered", retention:0.78, due: 2 }, { id:"limit_func", label:"Limit · function", kind:"definition", x:0.30, y:0.28, mastery:"mastered", retention:0.71, due: 1 }, { id:"epsilon_delta", label:"ε–δ definition", kind:"definition", x:0.48, y:0.34, mastery:"learning",retention:0.42, due: 0 }, { id:"limit_sum", label:"Limit · sum", kind:"lemma", x:0.66, y:0.28, mastery:"mastered", retention:0.84, due: 6 }, { id:"limit_product", label:"Limit · product", kind:"lemma", x:0.82, y:0.32, mastery:"learning",retention:0.51, due: -1 }, // review-due { id:"limit_compose", label:"Limit · composition", kind:"lemma", x:0.94, y:0.26, mastery:"learning",retention:0.46, due: -3 }, // review-due // continuity, secant, dq { id:"continuity", label:"Continuity", kind:"definition", x:0.18, y:0.48, mastery:"learning",retention:0.61, due: 0 }, { id:"secant", label:"Secant line", kind:"definition", x:0.40, y:0.50, mastery:"frontier",retention:null, due:null }, { id:"diff_quot", label:"Difference quotient", kind:"definition", x:0.58, y:0.46, mastery:"frontier",retention:null, due:null }, // DERIVATIVE — current frontier { id:"derivative", label:"Derivative", kind:"definition", x:0.50, y:0.62, mastery:"frontier", retention:null, due:null, focus:true }, // first-order theorems { id:"diff_implies_cont", label:"Diff ⇒ continuity", kind:"theorem", x:0.16, y:0.74, mastery:"locked", retention:null, due:null }, { id:"sum_rule", label:"Sum rule", kind:"theorem", x:0.34, y:0.78, mastery:"locked", retention:null, due:null }, { id:"product_rule", label:"Product rule", kind:"theorem", x:0.56, y:0.80, mastery:"locked", retention:null, due:null }, { id:"chain_rule", label:"Chain rule", kind:"theorem", x:0.74, y:0.74, mastery:"locked", retention:null, due:null }, // far downstream { id:"mvt", label:"Mean value theorem", kind:"theorem", x:0.10, y:0.92, mastery:"locked", retention:null, due:null }, { id:"l_hopital", label:"L'Hôpital's rule", kind:"theorem", x:0.30, y:0.94, mastery:"locked", retention:null, due:null }, { id:"taylor", label:"Taylor's theorem", kind:"theorem", x:0.56, y:0.94, mastery:"locked", retention:null, due:null }, { id:"newtons_method", label:"Newton's method", kind:"procedure", x:0.78, y:0.92, mastery:"locked", retention:null, due:null }, { id:"linear_approx", label:"Linear approximation",kind:"theorem", x:0.90, y:0.86, mastery:"locked", retention:null, due:null }, ]; const edges = [ ["set","function"],["function","limit_func"],["real_numbers","limit_seq"], ["limit_seq","limit_func"],["limit_func","epsilon_delta"],["limit_func","continuity"], ["limit_func","limit_sum"],["limit_func","limit_product"],["limit_func","limit_compose"], ["slope_line","secant"],["slope_line","diff_quot"],["function","secant"], ["composition","limit_compose"],["interval","derivative"],["absolute_val","continuity"], ["function","derivative"],["limit_func","derivative"],["diff_quot","derivative"], ["secant","derivative"],["continuity","derivative"], ["derivative","diff_implies_cont"],["derivative","sum_rule"],["derivative","product_rule"],["derivative","chain_rule"], ["limit_sum","sum_rule"],["limit_product","product_rule"],["limit_compose","chain_rule"], ["continuity","diff_implies_cont"], ["diff_implies_cont","mvt"],["derivative","mvt"],["derivative","l_hopital"], ["derivative","taylor"],["derivative","newtons_method"],["derivative","linear_approx"], ]; const nodeById = Object.fromEntries(nodes.map(n => [n.id, n])); const counts = { mastered: nodes.filter(n => n.mastery==="mastered" && n.retention >= 0.7).length, review: nodes.filter(n => n.due !== null && n.due < 0).length, learning: nodes.filter(n => n.mastery==="learning").length, frontier: nodes.filter(n => n.mastery==="frontier").length, locked: nodes.filter(n => n.mastery==="locked").length, }; // Effective state for visual encoding combines mastery + retention. const stateOf = (n) => { if (n.mastery === "frontier") return "frontier"; if (n.mastery === "locked") return "locked"; if (n.mastery === "learning") return "learning"; if (n.due !== null && n.due < 0) return "review"; return "mastered"; }; const matchFilter = (n) => filter === "all" || stateOf(n) === filter; const W = 1180, H = 720; const PAD_X = 60, PAD_Y = 40; const xToPx = (x) => PAD_X + x * (W - 2*PAD_X); const yToPx = (y) => PAD_Y + y * (H - 2*PAD_Y); const reviewDue = nodes.filter(n => n.due !== null && n.due < 0).sort((a,b) => a.due - b.due); const frontier = nodes.filter(n => n.mastery === "frontier"); return (