Skip to content

Commit 94bf68b

Browse files
author
Jose Cruz
committed
Fix course colors not persisting across pages
The graph API now stamps each node with its enrollment color (course_color) and normalizes the subject field from course_id, so the frontend no longer relies solely on a separately-fetched courseColorMap keyed by course_name. The KnowledgeGraph component now prefers the node's own course_color, falling back to the map. Dashboard refreshes graph data after a color change so the embedded graph reflects the update immediately.
1 parent c5a757d commit 94bf68b

4 files changed

Lines changed: 55 additions & 15 deletions

File tree

backend/services/graph_service.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,40 +137,60 @@ def get_graph(user_id: str) -> dict:
137137
"avg_learning_velocity": avg_velocity,
138138
}
139139

140+
# Build a course_id → color + name lookup from enrollments
141+
course_color_map: dict[str, str | None] = {}
142+
course_name_map: dict[str, str] = {}
143+
for enrollment in enrolled_courses:
144+
cid = enrollment["course_id"]
145+
course = enrollment.get("courses", {}) if isinstance(enrollment.get("courses"), dict) else {}
146+
course_color_map[cid] = enrollment.get("color")
147+
course_name_map[cid] = course.get("course_name", "")
148+
149+
# Stamp each node's subject from its course_id so the frontend has a
150+
# consistent key, and attach the enrollment color directly.
151+
for n in nodes:
152+
cid = n.get("course_id")
153+
if cid and cid in course_name_map:
154+
n["subject"] = course_name_map[cid]
155+
if cid and cid in course_color_map:
156+
n["course_color"] = course_color_map[cid]
157+
140158
# Build subject root hubs from enrolled courses
141159
subject_nodes = []
142160
subject_edges = []
143-
161+
144162
for enrollment in enrolled_courses:
145163
course_id = enrollment["course_id"]
146164
course = enrollment.get("courses", {}) if isinstance(enrollment.get("courses"), dict) else {}
147165
course_code = course.get("course_code", "")
148166
course_name = course.get("course_name", "")
149-
167+
150168
# Use "Course Code - Course Name" as the subject label
151169
subject_label = f"{course_code} - {course_name}" if course_code else course_name
152-
170+
153171
# Find all nodes belonging to this course
154172
subj_nodes = [n for n in nodes if n.get("course_id") == course_id]
155-
173+
156174
root_id = f"subject_root__{course_id}"
157175
if subj_nodes:
158176
avg_mastery = sum(n["mastery_score"] for n in subj_nodes) / len(subj_nodes)
159177
else:
160178
avg_mastery = 0.0
161-
179+
162180
subject_nodes.append({
163181
"id": root_id,
164182
"user_id": user_id,
165183
"concept_name": subject_label,
166184
"mastery_score": round(avg_mastery, 4),
167185
"mastery_tier": "subject_root",
168186
"course_id": course_id,
187+
"subject": course_name,
188+
"course_color": course_color_map.get(course_id),
169189
"times_studied": sum(n.get("times_studied", 0) for n in subj_nodes),
170190
"last_studied_at": None,
171191
"is_subject_root": True,
172192
})
173-
193+
174194
for n in subj_nodes:
175195
subject_edges.append({
176196
"id": f"subject_edge__{root_id}__{n['id']}",

frontend/src/app/dashboard/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,10 @@ function DashboardInner() {
339339
setCourseList(prev => prev.map(c => (c.course_id === courseId ? { ...c, color: newHex } : c)));
340340
setCourseColorMap(prev => ({ ...prev, [courseName]: newHex }));
341341
setEditingColorFor(null);
342+
// Refresh graph so nodes carry the updated course_color
343+
const graphData = await getGraph(userId);
344+
setNodes(graphData.nodes);
345+
setEdges(graphData.edges);
342346
} catch (e) {
343347
console.error(e);
344348
}

frontend/src/components/KnowledgeGraph.tsx

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface SimNode extends d3.SimulationNodeDatum {
2626
mastery_score: number;
2727
mastery_tier: string;
2828
subject: string;
29+
course_color?: string | null;
2930
is_subject_root?: boolean;
3031
}
3132

@@ -38,6 +39,12 @@ const ROOT_RADIUS = 22;
3839
const getSimRadius = (d: SimNode) =>
3940
d.is_subject_root ? ROOT_RADIUS : getNodeRadius(d.mastery_score);
4041

42+
/** Resolve the color override for a node: prefer the node's own course_color,
43+
* then fall back to the courseColorMap lookup by subject name. */
44+
function nodeColorOverride(d: SimNode, colorMap: Record<string, string>): string | undefined {
45+
return d.course_color ?? colorMap[d.subject] ?? undefined;
46+
}
47+
4148
/** Opacity encodes mastery tier — full colour = mastered, ghost = unexplored. */
4249
function masteryOpacity(tier: string): number {
4350
switch (tier) {
@@ -192,7 +199,7 @@ function KnowledgeGraph({
192199
.attr('font-size', d => d.is_subject_root ? '13px' : '11px')
193200
.attr('font-weight', d => d.is_subject_root ? '600' : '400')
194201
.attr('font-family', "'DM Sans', Inter, system-ui, sans-serif")
195-
.attr('fill', d => d.is_subject_root ? getCourseColor(d.subject, courseColorMapRef.current[d.subject]).text : '#374151')
202+
.attr('fill', d => d.is_subject_root ? getCourseColor(d.subject, nodeColorOverride(d, courseColorMapRef.current)).text : '#374151')
196203
.attr('pointer-events', 'none')
197204
.style('user-select', 'none');
198205

@@ -224,9 +231,9 @@ function KnowledgeGraph({
224231
const circles = nodeSel.append('circle')
225232
.attr('class', 'main-circle')
226233
.attr('r', d => getSimRadius(d))
227-
.attr('fill', d => getCourseColor(d.subject, courseColorMapRef.current[d.subject]).fill)
234+
.attr('fill', d => getCourseColor(d.subject, nodeColorOverride(d, courseColorMapRef.current)).fill)
228235
.attr('fill-opacity', d => masteryOpacity(d.mastery_tier))
229-
.attr('stroke', d => getCourseColor(d.subject, courseColorMapRef.current[d.subject]).fill)
236+
.attr('stroke', d => getCourseColor(d.subject, nodeColorOverride(d, courseColorMapRef.current)).fill)
230237
.attr('stroke-opacity', d => d.is_subject_root ? 0.7 : 0.4)
231238
.attr('stroke-width', d => d.is_subject_root ? 2.5 : 1.5);
232239

@@ -263,7 +270,7 @@ function KnowledgeGraph({
263270
const lastStudied = sourceNode.last_studied_at
264271
? new Date(sourceNode.last_studied_at).toLocaleDateString()
265272
: 'Never';
266-
const cc = getCourseColor(sourceNode.subject, courseColorMapRef.current[sourceNode.subject]);
273+
const cc = getCourseColor(sourceNode.subject, sourceNode.course_color ?? courseColorMapRef.current[sourceNode.subject]);
267274
tooltip.innerHTML = `
268275
<div style="font-weight:600;color:#111827;margin-bottom:4px">${sourceNode.concept_name}</div>
269276
<div style="display:flex;align-items:center;gap:5px;margin-bottom:4px">
@@ -370,17 +377,25 @@ function KnowledgeGraph({
370377
const svg = d3.select(svgRef.current);
371378
svg.selectAll<SVGCircleElement, SimNode>('.main-circle')
372379
.attr('fill', d => {
373-
const subj = nodeMap.get(d.id)?.subject ?? d.subject;
374-
return getCourseColor(subj, courseColorMap[subj]).fill;
380+
const n = nodeMap.get(d.id);
381+
const subj = n?.subject ?? d.subject;
382+
const override = n?.course_color ?? d.course_color ?? courseColorMap[subj];
383+
return getCourseColor(subj, override).fill;
375384
})
376385
.attr('fill-opacity', d => masteryOpacity(nodeMap.get(d.id)?.mastery_tier ?? d.mastery_tier))
377386
.attr('stroke', d => {
378-
const subj = nodeMap.get(d.id)?.subject ?? d.subject;
379-
return getCourseColor(subj, courseColorMap[subj]).fill;
387+
const n = nodeMap.get(d.id);
388+
const subj = n?.subject ?? d.subject;
389+
const override = n?.course_color ?? d.course_color ?? courseColorMap[subj];
390+
return getCourseColor(subj, override).fill;
380391
});
381392
svg.selectAll<SVGTextElement, SimNode>('text')
382393
.filter(d => !!d.is_subject_root)
383-
.attr('fill', d => getCourseColor(d.subject, courseColorMap[d.subject]).text);
394+
.attr('fill', d => {
395+
const n = nodeMap.get(d.id);
396+
const override = n?.course_color ?? d.course_color ?? courseColorMap[d.subject];
397+
return getCourseColor(d.subject, override).text;
398+
});
384399
}, [nodes, courseColorMap]);
385400

386401
// Separate lightweight effect: only add/remove the highlight ring.

frontend/src/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface GraphNode {
77
last_studied_at: string | null;
88
subject: string;
99
course_id?: string | null;
10+
course_color?: string | null;
1011
is_subject_root?: boolean;
1112
x?: number;
1213
y?: number;

0 commit comments

Comments
 (0)