Skip to content

Commit e57098e

Browse files
committed
Enhance Graphviz components to track total and repeat visits for edges
- Updated GraphvizParent to accept totalVisits and repeatVisits props. - Modified countEdges function to calculate total visits and track repeat visits for each edge. - Adjusted generateDotString to include total visit counts and repeat visit information in tooltips for edges. These changes improve the visualization by providing more detailed insights into student interactions with edges.
1 parent 6ffa747 commit e57098e

3 files changed

Lines changed: 78 additions & 52 deletions

File tree

README.md

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -102,24 +102,3 @@ Time,Step Name,Outcome,CF (Workspace Progress Status),Problem Name,Anon Student
102102
- Blue: Hint-related transitions (INITIAL_HINT, HINT_LEVEL_CHANGE)
103103
- Yellow: Just-in-time feedback (JIT, FREEBIE_JIT)
104104

105-
## Expanding the ESLint configuration
106-
107-
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
108-
109-
- Configure the top-level `parserOptions` property like this:
110-
111-
```js
112-
export default {
113-
// other rules...
114-
parserOptions: {
115-
ecmaVersion: 'latest',
116-
sourceType: 'module',
117-
project: ['./tsconfig.json', './tsconfig.node.json'],
118-
tsconfigRootDir: __dirname,
119-
},
120-
}
121-
```
122-
123-
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
124-
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
125-
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

src/components/GraphvizParent.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
5151
ratioEdges,
5252
edgeOutcomeCounts,
5353
maxEdgeCount,
54+
totalVisits,
55+
repeatVisits,
5456
topSequences
5557
} = countEdges(stepSequences, outcomeSequences);
5658

@@ -76,7 +78,9 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
7678
1,
7779
minVisits,
7880
selectedSequence,
79-
false
81+
false,
82+
totalVisits,
83+
repeatVisits
8084
)
8185
);
8286

@@ -90,7 +94,9 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
9094
1,
9195
minVisits,
9296
selectedSequence,
93-
true
97+
true,
98+
totalVisits,
99+
repeatVisits
94100
)
95101
);
96102
}
@@ -109,6 +115,8 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
109115
ratioEdges: filteredRatioEdges,
110116
edgeOutcomeCounts: filteredEdgeOutcomeCounts,
111117
maxEdgeCount: filteredMaxEdgeCount,
118+
totalVisits: filteredTotalVisits,
119+
repeatVisits: filteredRepeatVisits,
112120
} = countEdges(filteredStepSequences, filteredOutcomeSequences);
113121

114122
const filteredNormalizedThicknesses = normalizeThicknesses(filteredEdgeCounts, filteredMaxEdgeCount, 10);
@@ -123,7 +131,9 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
123131
1,
124132
minVisits,
125133
selectedSequence,
126-
false
134+
false,
135+
filteredTotalVisits,
136+
filteredRepeatVisits
127137
)
128138
);
129139
} else {

src/components/GraphvizProcessing.ts

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -169,24 +169,26 @@ export const countEdges = (
169169
totalNodeEdges: { [p: string]: number };
170170
edgeOutcomeCounts: { [p: string]: { [p: string]: number } };
171171
maxEdgeCount: number;
172-
ratioEdges: { [p: string]: number };
172+
ratioEdges: { [key: string]: number };
173173
edgeCounts: { [key: string]: number };
174+
totalVisits: { [key: string]: number };
175+
repeatVisits: { [key: string]: { [studentId: string]: number } };
174176
topSequences: SequenceCount[];
175177
} => {
176-
const totalNodeEdges: { [key: string]: number } = {};
178+
const totalNodeEdges: { [key: string]: Set<string> } = {};
177179
const edgeOutcomeCounts: { [key: string]: { [outcome: string]: number } } = {};
178180
let maxEdgeCount = 0;
179181
const ratioEdges: { [key: string]: number } = {};
180182
const edgeCounts: { [key: string]: number } = {};
181-
const studentEdgeCounts: { [key: string]: Set<string> } = {}; // Track unique students per edge
183+
const totalVisits: { [key: string]: number } = {};
184+
const studentEdgeCounts: { [key: string]: Set<string> } = {};
185+
const repeatVisits: { [key: string]: { [studentId: string]: number } } = {};
182186
const top5Sequences = getTopSequences(stepSequences, 5);
183187

184-
// Iterate over first-level keys (student IDs)
185188
Object.keys(stepSequences).forEach((studentId) => {
186189
const innerStepSequences = stepSequences[studentId];
187190
const innerOutcomeSequences = outcomeSequences[studentId] || {};
188191

189-
// Iterate over second-level keys (problem names)
190192
Object.keys(innerStepSequences).forEach((problemName) => {
191193
const steps = innerStepSequences[problemName];
192194
const outcomes = innerOutcomeSequences[problemName] || [];
@@ -200,20 +202,30 @@ export const countEdges = (
200202

201203
const edgeKey = `${currentStep}->${nextStep}`;
202204

203-
// Initialize the Set for this edge if it doesn't exist
204205
if (!studentEdgeCounts[edgeKey]) {
205206
studentEdgeCounts[edgeKey] = new Set();
206207
}
208+
if (!totalNodeEdges[currentStep]) {
209+
totalNodeEdges[currentStep] = new Set();
210+
}
211+
if (!totalVisits[edgeKey]) {
212+
totalVisits[edgeKey] = 0;
213+
}
214+
if (!repeatVisits[edgeKey]) {
215+
repeatVisits[edgeKey] = {};
216+
}
207217

208-
// Add this student to the Set for this edge
209218
studentEdgeCounts[edgeKey].add(studentId);
219+
totalNodeEdges[currentStep].add(studentId);
220+
totalVisits[edgeKey]++;
221+
222+
// Track repeat visits for all edges
223+
repeatVisits[edgeKey][studentId] = (repeatVisits[edgeKey][studentId] || 0) + 1;
210224

211-
// Update edge counts based on unique students
212225
edgeCounts[edgeKey] = studentEdgeCounts[edgeKey].size;
213226

214227
edgeOutcomeCounts[edgeKey] = edgeOutcomeCounts[edgeKey] || {};
215228
edgeOutcomeCounts[edgeKey][outcome] = (edgeOutcomeCounts[edgeKey][outcome] || 0) + 1;
216-
totalNodeEdges[currentStep] = (totalNodeEdges[currentStep] || 0) + 1;
217229

218230
if (edgeCounts[edgeKey] > maxEdgeCount) {
219231
maxEdgeCount = edgeCounts[edgeKey];
@@ -222,18 +234,24 @@ export const countEdges = (
222234
});
223235
});
224236

225-
// Compute ratioEdges based on totalNodeEdges
237+
const totalNodeEdgesCounts: { [key: string]: number } = {};
238+
Object.keys(totalNodeEdges).forEach(node => {
239+
totalNodeEdgesCounts[node] = totalNodeEdges[node].size;
240+
});
241+
226242
Object.keys(edgeCounts).forEach((edge) => {
227243
const [start] = edge.split('->');
228-
ratioEdges[edge] = edgeCounts[edge] / (totalNodeEdges[start] || 1);
244+
ratioEdges[edge] = edgeCounts[edge] / (totalNodeEdgesCounts[start] || 1);
229245
});
230246

231247
return {
232248
edgeCounts,
233-
totalNodeEdges,
249+
totalNodeEdges: totalNodeEdgesCounts,
234250
ratioEdges,
235251
edgeOutcomeCounts,
236252
maxEdgeCount,
253+
totalVisits,
254+
repeatVisits,
237255
topSequences: top5Sequences
238256
};
239257
};
@@ -352,90 +370,109 @@ function calculateEdgeColors(outcomes: { [outcome: string]: number }): string {
352370
*
353371
* @param justTopSequence
354372
* @returns A string in Graphviz DOT format that represents the graph.
355-
*/export function generateDotString(
373+
*/
374+
export function generateDotString(
356375
normalizedThicknesses: { [key: string]: number },
357-
// mostCommonSequence: string[],
358376
ratioEdges: { [key: string]: number },
359377
edgeOutcomeCounts: EdgeCounts['edgeOutcomeCounts'],
360378
edgeCounts: EdgeCounts['edgeCounts'],
361379
totalNodeEdges: EdgeCounts['totalNodeEdges'],
362-
threshold: number, //Make a percentage to be more intuitive
380+
threshold: number,
363381
min_visits: number,
364382
selectedSequence: SequenceCount["sequence"],
365-
justTopSequence: boolean
383+
justTopSequence: boolean,
384+
totalVisits: { [key: string]: number },
385+
repeatVisits: { [key: string]: { [studentId: string]: number } }
366386
): string {
367387
if (!selectedSequence || selectedSequence.length === 0) {
368388
return 'digraph G {\n"Error" [label="No valid sequences found to display."];\n}';
369389
}
370390

371391
let dotString = 'digraph G {\ngraph [size="8,6!", dpi=150];\n';
372-
let totalSteps = selectedSequence.length//stepsInSelectedSequence.length;
373-
let steps = selectedSequence
392+
let totalSteps = selectedSequence.length;
393+
let steps = selectedSequence;
374394

375395
if (justTopSequence) {
376396
for (let rank = 0; rank < totalSteps; rank++) {
377397
const currentStep = steps[rank];
378398
const nextStep = steps[rank + 1];
379399
const edgeKey = `${currentStep}->${nextStep}`;
380-
const thickness = normalizedThicknesses[edgeKey] || 1; // Default thickness if not present
400+
const thickness = normalizedThicknesses[edgeKey] || 1;
381401
const outcomes = edgeOutcomeCounts[edgeKey] || {};
382402
const edgeCount = edgeCounts[edgeKey] || 0;
403+
const visits = totalVisits[edgeKey] || 0;
383404
const totalCount = totalNodeEdges[currentStep] || 0;
384-
const color = calculateColor(rank, totalSteps)
405+
const color = calculateColor(rank, totalSteps);
385406
const edgeColor = calculateEdgeColors(outcomes);
386407
const node_tooltip = `Rank:\n\t\t ${rank + 1}\nColor:\n\t\t ${color}`;
387408

388409
dotString += ` "${currentStep}" [rank=${rank + 1}, style=filled, fillcolor="${color}", tooltip="${node_tooltip}"];\n`;
389410

390411
if (edgeCount > min_visits) {
391-
const tooltip = `${currentStep} to ${nextStep}\n`
412+
let tooltip = `${currentStep} to ${nextStep}\n`
392413
+ `- Unique Students: \n\t\t ${edgeCount}\n`
414+
+ `- Total Edge Visits: \n\t\t ${visits}\n`
393415
+ `- Total Students at ${currentStep}: \n\t\t${totalNodeEdges[currentStep] || 0}\n`
394416
+ `- Ratio: \n\t\t${((ratioEdges[edgeKey] || 0) * 100).toFixed(2)}% of students at ${currentStep} go to ${nextStep}\n`
395417
+ `- Outcomes: \n\t\t ${Object.entries(outcomes).map(([outcome, count]) => `${outcome}: ${count}`).join('\n\t\t ')}\n`
396418
+ `- Color Codes: \n\t\t Hex: ${color}`;
397419

420+
// Add repeat visit information for all edges
421+
if (repeatVisits[edgeKey]) {
422+
const repeatCounts = Object.values(repeatVisits[edgeKey]);
423+
const studentsWithRepeats = repeatCounts.filter(count => count > 1).length;
424+
const maxRepeats = Math.max(...repeatCounts);
425+
tooltip += `\n- Repeat Visits:\n\t\t ${studentsWithRepeats} students visited this edge multiple times\n\t\t Maximum visits by a student: ${maxRepeats}`;
426+
}
427+
398428
dotString += ` "${currentStep}" -> "${nextStep}" [penwidth=${thickness}, color="${edgeColor}", tooltip="${tooltip}"];\n`;
399429
}
400430
}
401431
} else {
402-
console.log(totalSteps, steps)
403432
for (let rank = 0; rank < totalSteps; rank++) {
404433
const step = steps[rank];
405434
const color = calculateColor(rank, totalSteps);
406-
console.log(step, color)
407435
const node_tooltip = `Rank:\n\t\t ${rank + 1}\nColor:\n\t\t ${color}`;
408436

409437
dotString += ` "${step}" [rank=${rank + 1}, style=filled, fillcolor="${color}", tooltip="${node_tooltip}"];\n`;
410438
}
411-
// Create edge definitions in the DOT string based on normalized thickness and thresholds
439+
412440
for (const edge of Object.keys(normalizedThicknesses)) {
413441
if (normalizedThicknesses[edge] >= threshold) {
414442
const [currentStep, nextStep] = edge.split('->');
415443
const thickness = normalizedThicknesses[edge];
416444
const outcomes = edgeOutcomeCounts[edge] || {};
417445
const edgeCount = edgeCounts[edge] || 0;
446+
const visits = totalVisits[edge] || 0;
418447
const totalCount = totalNodeEdges[currentStep] || 0;
419448
const color = calculateEdgeColors(outcomes);
420449
const outcomesStr = Object.entries(outcomes)
421450
.map(([outcome, count]) => `${outcome}: ${count}`)
422451
.join('\n\t\t ');
423452

424453
if (edgeCount > min_visits) {
425-
const tooltip = `${currentStep} to ${nextStep}\n`
426-
+ `- Unique Students: \n\t\t ${edgeCount}\n`
427-
+ `- Total Students at ${currentStep}: \n\t\t${totalNodeEdges[currentStep] || 0}\n`
454+
let tooltip = `- Total Students at ${currentStep}: \n\t\t${totalNodeEdges[currentStep] || 0}\n\n`
455+
+ `${currentStep} to ${nextStep}\n`
456+
+ `- Unique Students on edge (each student only counted once): \n\t\t ${edgeCount}\n`
457+
+ `- Edge taken ${visits} times\n`
428458
+ `- Ratio: \n\t\t${((ratioEdges[edge] || 0) * 100).toFixed(2)}% of students at ${currentStep} go to ${nextStep}\n`
429459
+ `- Outcomes: \n\t\t ${outcomesStr}\n`
430460
+ `- Color Codes: \n\t\t Hex: ${color}\n\t\t RGB: ${[parseInt(color.substring(1, 3), 16), parseInt(color.substring(3, 5), 16), parseInt(color.substring(5, 7), 16)]}`;
431461

462+
// Add repeat visit information for all edges
463+
if (repeatVisits[edge]) {
464+
const repeatCounts = Object.values(repeatVisits[edge]);
465+
const studentsWithRepeats = repeatCounts.filter(count => count > 1).length;
466+
const maxRepeats = Math.max(...repeatCounts);
467+
tooltip += `\n- Repeat Visits:\n\t\t ${studentsWithRepeats} students visited this edge multiple times\n\t\t Maximum visits by a student: ${maxRepeats}`;
468+
}
469+
432470
dotString += ` "${currentStep}" -> "${nextStep}" [penwidth=${thickness}, color="${color}", tooltip="${tooltip}"];\n`;
433471
}
434472
}
435473
}
436474
}
437475

438-
439476
dotString += '}';
440477
return dotString;
441478
}

0 commit comments

Comments
 (0)