Skip to content

Commit a1657d5

Browse files
committed
added option to not color nodes by selected sequence
1 parent 686ca45 commit a1657d5

File tree

3 files changed

+80
-27
lines changed

3 files changed

+80
-27
lines changed

src/components/GraphMenu.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ interface GraphMenuProps {
1212
showSequenceFilter?: boolean;
1313
showOnlySequenceStudents?: boolean;
1414
onSequenceFilterChange?: (value: boolean) => void;
15+
// Optional props for node coloring checkbox
16+
showNodeColoringOption?: boolean;
17+
colorNodesBySequence?: boolean;
18+
onNodeColoringChange?: (value: boolean) => void;
1519
}
1620

1721
const GraphMenu: React.FC<GraphMenuProps> = ({
@@ -22,7 +26,10 @@ const GraphMenu: React.FC<GraphMenuProps> = ({
2226
showSlider = true,
2327
showSequenceFilter = false,
2428
showOnlySequenceStudents = true,
25-
onSequenceFilterChange
29+
onSequenceFilterChange,
30+
showNodeColoringOption = false,
31+
colorNodesBySequence = true,
32+
onNodeColoringChange
2633
}) => {
2734
const [isOpen, setIsOpen] = useState(false);
2835

@@ -58,6 +65,22 @@ const GraphMenu: React.FC<GraphMenuProps> = ({
5865
</div>
5966
)}
6067

68+
{showNodeColoringOption && onNodeColoringChange && (
69+
<div className="mb-4">
70+
<label className="flex items-center cursor-pointer">
71+
<input
72+
type="checkbox"
73+
checked={colorNodesBySequence}
74+
onChange={(e) => onNodeColoringChange(e.target.checked)}
75+
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
76+
/>
77+
<span className="ml-2 text-xs font-medium text-gray-700">
78+
Color nodes by sequence position
79+
</span>
80+
</label>
81+
</div>
82+
)}
83+
6184
{showSlider && (
6285
<>
6386
<div className="flex justify-between items-center mb-3">

src/components/GraphvizParent.tsx

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
7373
// State for selected sequence graph filtering
7474
const [showOnlySequenceStudents, setShowOnlySequenceStudents] = useState<boolean>(true);
7575

76+
// State for node coloring
77+
const [colorNodesBySequence, setColorNodesBySequence] = useState<boolean>(true);
78+
7679
// History state management
7780
const [activeTab, setActiveTab] = useState<'graphs' | 'history'>('graphs');
7881
const [historyItems, setHistoryItems] = useState<HistoryItem[]>([]);
@@ -188,6 +191,7 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
188191
errorMode, // Use actual errorMode setting
189192
mainGraphData.firstAttemptOutcomes,
190193
uniqueStudentMode,
194+
colorNodesBySequence,
191195
);
192196

193197
setDotString(dotString);
@@ -223,10 +227,11 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
223227
false, // Force errorMode to false for static top graph
224228
sequenceResults.firstAttemptOutcomes,
225229
uniqueStudentMode,
230+
colorNodesBySequence,
226231
)
227232
);
228233
}
229-
}, [mainGraphData, selectedSequence, setTop5Sequences, top5Sequences, onMaxEdgeCountChange, onMaxMinEdgeCountChange, uniqueStudentMode, minVisits, errorMode, minVisitsPerGraph, showOnlySequenceStudents]); // Responds to uniqueStudentMode, minVisits, errorMode, minVisitsPerGraph, showOnlySequenceStudents and selectedSequence
234+
}, [mainGraphData, selectedSequence, setTop5Sequences, top5Sequences, onMaxEdgeCountChange, onMaxMinEdgeCountChange, uniqueStudentMode, minVisits, errorMode, minVisitsPerGraph, showOnlySequenceStudents, colorNodesBySequence]); // Responds to uniqueStudentMode, minVisits, errorMode, minVisitsPerGraph, showOnlySequenceStudents, colorNodesBySequence and selectedSequence
230235

231236
// Memoized filtered graph data for each filter
232237
const filteredGraphDataMap = useMemo(() => {
@@ -344,6 +349,7 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
344349
errorMode,
345350
filteredGraphData.firstAttemptOutcomes,
346351
uniqueStudentMode,
352+
colorNodesBySequence,
347353
);
348354

349355
newFilteredDotStrings[filter] = filteredDotString;
@@ -375,19 +381,23 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
375381
}, []);
376382

377383
// Export a graph as high-quality PNG with problem name
378-
const exportGraphAsPNG = (graphRef: React.RefObject<HTMLDivElement>, graphName: string, minStudents?: number) => {
384+
const exportGraphAsPNG = (graphRef: React.RefObject<HTMLDivElement>, graphName: string, minStudents?: number, isColored?: boolean) => {
379385
if (!graphRef.current) return;
380386

381387
const svgElement = graphRef.current.querySelector('svg') as SVGSVGElement;
382388
if (!svgElement) return;
383389

384-
// Build filename: problemName_graphName_minXX
390+
// Build filename: problemName_graphName_minXX_colored
385391
const sanitizedProblemName = problemName.replace(/[^a-zA-Z0-9]/g, '_');
386392
const sanitizedGraphName = graphName.replace(/[^a-zA-Z0-9]/g, '_');
387393
let filename = `${sanitizedProblemName}_${sanitizedGraphName}`;
388394
if (minStudents !== undefined) {
389395
filename += `_min${minStudents}`;
390396
}
397+
if (isColored) {
398+
filename += '_colored';
399+
}
400+
console.log('Export PNG - isColored:', isColored, 'filename:', filename);
391401

392402
// Get the actual content bounding box to capture the entire visible graph
393403
const gElement = svgElement.querySelector('g') as SVGGElement;
@@ -484,10 +494,10 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
484494
// Helper function to calculate progress status statistics for students at a node
485495
const calculateNodeProgressStats = (nodeName: string): { graduated: number; promoted: number; other: number; total: number } => {
486496
if (!mainGraphData) return { graduated: 0, promoted: 0, other: 0, total: 0 };
487-
497+
488498
const { stepSequences, sortedData } = mainGraphData;
489499
const studentsAtNode = new Set<string>();
490-
500+
491501
// Find all students who visited this node
492502
// stepSequences has structure: { [studentId]: { [problemName]: string[] } }
493503
if (stepSequences && Object.keys(stepSequences).length > 0) {
@@ -502,26 +512,30 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
502512
}
503513
});
504514
}
505-
515+
506516
// Count progress status for students who visited this node
517+
// IMPORTANT: Filter by current problemName to get accurate progress status
507518
let graduatedCount = 0;
508519
let promotedCount = 0;
509520
let otherCount = 0;
510-
521+
511522
if (sortedData && sortedData.length > 0) {
512-
// Create a map of all students and their progress status
523+
// Create a map of students and their progress status FOR THIS PROBLEM ONLY
513524
const studentProgressMap = new Map<string, string>();
514525
sortedData.forEach((row: any) => {
515526
const studentId = row['Anon Student Id'];
527+
const rowProblemName = row['Problem Name'];
516528
const progressStatus = row['CF (Workspace Progress Status)'];
517-
if (studentId && progressStatus) {
529+
530+
// Only use progress status from rows for the current problem
531+
if (studentId && progressStatus && rowProblemName === problemName) {
518532
studentProgressMap.set(studentId, progressStatus);
519533
}
520534
});
521-
535+
522536
studentsAtNode.forEach(studentId => {
523537
const progressStatus = studentProgressMap.get(studentId);
524-
538+
525539
if (progressStatus === 'GRADUATED') {
526540
graduatedCount++;
527541
} else if (progressStatus === 'PROMOTED') {
@@ -531,7 +545,7 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
531545
}
532546
});
533547
}
534-
548+
535549
return {
536550
graduated: graduatedCount,
537551
promoted: promotedCount,
@@ -728,21 +742,25 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
728742
}
729743

730744
// Count exact progress status for students who took this transition
745+
// IMPORTANT: Filter by current problemName to get accurate progress status
731746
let graduatedCount = 0;
732747
let promotedCount = 0;
733748
let otherCount = 0;
734-
749+
735750
if (sortedData && sortedData.length > 0 && studentsOnEdge.size > 0) {
736-
// Create a map of student progress status
751+
// Create a map of student progress status FOR THIS PROBLEM ONLY
737752
const studentProgressMap = new Map<string, string>();
738753
sortedData.forEach((row: any) => {
739754
const studentId = row['Anon Student Id'];
755+
const rowProblemName = row['Problem Name'];
740756
const progressStatus = row['CF (Workspace Progress Status)'];
741-
if (studentId && progressStatus) {
757+
758+
// Only use progress status from rows for the current problem
759+
if (studentId && progressStatus && rowProblemName === problemName) {
742760
studentProgressMap.set(studentId, progressStatus);
743761
}
744762
});
745-
763+
746764
studentsOnEdge.forEach(studentId => {
747765
const progressStatus = studentProgressMap.get(studentId);
748766
if (progressStatus === 'GRADUATED') {
@@ -1449,11 +1467,14 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
14491467
showSequenceFilter={true}
14501468
showOnlySequenceStudents={showOnlySequenceStudents}
14511469
onSequenceFilterChange={(value: boolean) => setShowOnlySequenceStudents(value)}
1470+
showNodeColoringOption={true}
1471+
colorNodesBySequence={colorNodesBySequence}
1472+
onNodeColoringChange={(value: boolean) => setColorNodesBySequence(value)}
14521473
/>
14531474
<div ref={graphRefTop} className="w-full h-full"></div>
14541475
</div>
14551476
<div className="w-full flex justify-center mt-2">
1456-
<ExportButton onClick={() => exportGraphAsPNG(graphRefTop, 'selected_sequence', minVisitsPerGraph['selected_sequence'])} />
1477+
<ExportButton onClick={() => exportGraphAsPNG(graphRefTop, 'selected_sequence', minVisitsPerGraph['selected_sequence'], colorNodesBySequence)} />
14571478
</div>
14581479
</div>
14591480
)}
@@ -1471,7 +1492,7 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
14711492
<div ref={graphRefMain} className="w-full h-full"></div>
14721493
</div>
14731494
<div className="w-full flex justify-center mt-2">
1474-
<ExportButton onClick={() => exportGraphAsPNG(graphRefMain, 'all_students', minVisitsPerGraph['all_students'] ?? Math.round((mainGraphData?.maxEdgeCount || 100) * 0.1))} />
1495+
<ExportButton onClick={() => exportGraphAsPNG(graphRefMain, 'all_students', minVisitsPerGraph['all_students'] ?? Math.round((mainGraphData?.maxEdgeCount || 100) * 0.1), colorNodesBySequence)} />
14751496
</div>
14761497
</div>
14771498
)}
@@ -1497,7 +1518,7 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
14971518
<div ref={ref} className="w-full h-full"></div>
14981519
</div>
14991520
<div className="w-full flex justify-center mt-2">
1500-
<ExportButton onClick={() => exportGraphAsPNG(ref, `filtered_graph_${filter}`, minVisitsPerGraph[graphKey] ?? Math.round((filteredGraphData?.maxEdgeCount || 100) * 0.1))} />
1521+
<ExportButton onClick={() => exportGraphAsPNG(ref, `filtered_graph_${filter}`, minVisitsPerGraph[graphKey] ?? Math.round((filteredGraphData?.maxEdgeCount || 100) * 0.1), colorNodesBySequence)} />
15011522
</div>
15021523
</div>
15031524
);

src/components/GraphvizProcessing.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ const initializeEdgeTracking = (
376376
const updateEdgeMetrics = (
377377
edgeKey: string,
378378
currentStep: string,
379+
_nextStep: string,
379380
studentId: string,
380381
outcome: string,
381382
maps: EdgeTrackingMaps,
@@ -434,6 +435,7 @@ const processStudentPaths = (
434435
maxEdgeCount = updateEdgeMetrics(
435436
edgeKey,
436437
currentStep,
438+
nextStep,
437439
studentId,
438440
outcome,
439441
maps,
@@ -655,6 +657,7 @@ export const countEdgesForSelectedSequence = (
655657
maxEdgeCount = updateEdgeMetrics(
656658
edgeKey,
657659
currentStep,
660+
nextStep,
658661
studentId,
659662
outcome,
660663
trackingMaps,
@@ -701,6 +704,7 @@ export const countEdgesForSelectedSequence = (
701704
maxEdgeCount = updateEdgeMetrics(
702705
edgeKey,
703706
currentStep,
707+
nextStep,
704708
studentId,
705709
outcome,
706710
trackingMaps,
@@ -1261,14 +1265,15 @@ const generateTopSequenceVisualization = (
12611265
repeatVisits: { [key: string]: { [studentId: string]: number } },
12621266
minVisits: number,
12631267
errorMode: boolean,
1264-
uniqueStudentMode: boolean = false
1268+
uniqueStudentMode: boolean = false,
1269+
colorNodesBySequence: boolean = true
12651270
): string => {
12661271
let dotContent = '';
12671272
const totalSteps = selectedSequence.length;
12681273

12691274
for (let rank = 0; rank < totalSteps; rank++) {
12701275
const currentStep = selectedSequence[rank];
1271-
const color = calculateColor(rank, totalSteps);
1276+
const color = colorNodesBySequence ? calculateColor(rank, totalSteps) : '#ffffff';
12721277
const studentCount = totalNodeEdges[currentStep] || 0;
12731278
const nodeTooltip = createNodeTooltip(rank, color, studentCount);
12741279

@@ -1333,7 +1338,8 @@ const generateFullGraphVisualization = (
13331338
threshold: number,
13341339
minVisits: number,
13351340
errorMode: boolean,
1336-
uniqueStudentMode: boolean = false
1341+
uniqueStudentMode: boolean = false,
1342+
colorNodesBySequence: boolean = true
13371343
): string => {
13381344
let dotContent = '';
13391345
const totalSteps = selectedSequence.length;
@@ -1358,7 +1364,7 @@ const generateFullGraphVisualization = (
13581364

13591365
for (const nodeName of allNodesInEdges) {
13601366
const sequenceRank = selectedSequence.indexOf(nodeName);
1361-
const color = sequenceRank >= 0 ? calculateColor(sequenceRank, totalSteps) : '#ffffff';
1367+
const color = (colorNodesBySequence && sequenceRank >= 0) ? calculateColor(sequenceRank, totalSteps) : '#ffffff';
13621368
const rank = sequenceRank >= 0 ? sequenceRank + 1 : 0;
13631369
const studentCount = totalNodeEdges[nodeName] || 0;
13641370
const nodeTooltip = createNodeTooltip(sequenceRank, color, studentCount);
@@ -1438,7 +1444,8 @@ export function generateDotString(
14381444
repeatVisits: { [key: string]: { [studentId: string]: number } },
14391445
errorMode: boolean,
14401446
firstAttemptOutcomes: { [key: string]: { [outcome: string]: number } },
1441-
uniqueStudentMode: boolean = false
1447+
uniqueStudentMode: boolean = false,
1448+
colorNodesBySequence: boolean = true
14421449
): string {
14431450
if (!selectedSequence || selectedSequence.length === 0) {
14441451
return 'digraph G {\n"Error" [label="No valid sequences found to display."];\n}';
@@ -1468,7 +1475,8 @@ export function generateDotString(
14681475
repeatVisits,
14691476
minVisits,
14701477
errorMode,
1471-
uniqueStudentMode
1478+
uniqueStudentMode,
1479+
colorNodesBySequence
14721480
);
14731481
} else {
14741482
dotString += generateFullGraphVisualization(
@@ -1484,7 +1492,8 @@ export function generateDotString(
14841492
threshold,
14851493
minVisits,
14861494
errorMode,
1487-
uniqueStudentMode
1495+
uniqueStudentMode,
1496+
colorNodesBySequence
14881497
);
14891498
}
14901499

0 commit comments

Comments
 (0)