Skip to content

Commit 7cd3727

Browse files
committed
feat: show actual student count in edge visit threshold slider
- Add maxEdgeCount prop to Slider component - Display both percentage and actual student count in slider label - Update App component to pass maxEdgeCount to Slider - Calculate actual student count from percentage and maxEdgeCount - Added a new studentEdgeCounts object that uses Sets to track unique students per edge This change makes the edge visit threshold more intuitive by showing the actual number of students that would be required for an edge to be displayed, rather than just showing the percentage.
1 parent ca6c223 commit 7cd3727

5 files changed

Lines changed: 52 additions & 34 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ React + TypeScript + Vite + SWC (with Rust compiler)
1616
The application accepts the following file formats:
1717
- CSV (Comma Separated Values)
1818
- TSV (Tab Separated Values)
19-
- JSON
2019

2120
### Required Fields
2221
Your data file must include the following fields:

src/App.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,19 @@ function App() {
2424
// State to toggle whether self-loops (transitions back to the same node) should be included
2525
const [selfLoops, setSelfLoops] = useState<boolean>(true);
2626
// State to manage the minimum number of visits for displaying edges in the graph
27-
const [minVisits, setMinVisits] = useState<number>(0);
27+
const [minVisitsPercentage, setMinVisitsPercentage] = useState<number>(0);
2828
const { resetData, loading, error, top5Sequences, setSelectedSequence, selectedSequence, csvData, setCSVData } = useContext(Context);
29+
const [maxEdgeCount, setMaxEdgeCount] = useState<number>(100); // Default value
30+
2931
const showControls = useMemo(() => {
3032
return !loading && csvData.length > 0;
31-
3233
}, [loading, csvData]);
3334

34-
3535
const handleSelectSequence = (selectedSequence: SequenceCount["sequence"]) => {
36-
console.log("SS: ", top5Sequences, selectedSequence);
37-
3836
if (top5Sequences) {
39-
// Update the selected sequence in the context
4037
setSelectedSequence(selectedSequence);
41-
console.log(`Selected sequence: ${selectedSequence}`);
4238
}
43-
4439
};
45-
// TODO: Implement error handling
46-
// const handleError = (errorMessage: string) => {
47-
// setError(errorMessage);
48-
// }
4940

5041
/**
5142
* Toggles the self-loops inclusion in the graph by switching the state.
@@ -57,7 +48,9 @@ function App() {
5748
*
5849
* @param {number} value - The new value for minimum visits.
5950
*/
60-
const handleSlider = (value: number) => setMinVisits(value);
51+
const handleSlider = (value: number) => {
52+
setMinVisitsPercentage(value);
53+
};
6154

6255
/**
6356
* Updates the `csvData` state with the uploaded CSV data when the file is processed.
@@ -66,6 +59,9 @@ function App() {
6659
*/
6760
const handleDataProcessed = (uploadedCsvData: string) => setCSVData(uploadedCsvData);
6861

62+
// Calculate actual min visits from percentage
63+
const minVisits = Math.round((minVisitsPercentage / 100) * maxEdgeCount);
64+
6965
/**
7066
* Updates the loading state when the file upload or processing begins or ends.
7167
*
@@ -143,11 +139,12 @@ function App() {
143139
<div className="space-y-2">
144140
<label className="text-sm font-medium text-gray-700">Edge Visits</label>
145141
<Slider
146-
step={5}
142+
step={1}
147143
min={0}
148-
max={5000}
149-
value={minVisits}
144+
max={100}
145+
value={minVisitsPercentage}
150146
onChange={handleSlider}
147+
maxEdgeCount={maxEdgeCount}
151148
/>
152149
</div>
153150
</div>
@@ -167,6 +164,7 @@ function App() {
167164
filter={filter}
168165
selfLoops={selfLoops}
169166
minVisits={minVisits}
167+
onMaxEdgeCountChange={setMaxEdgeCount}
170168
/>
171169
</div>
172170
</div>

src/components/GraphvizParent.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ interface GraphvizParentProps {
1919
filter: string | null;
2020
selfLoops: boolean;
2121
minVisits: number;
22+
onMaxEdgeCountChange: (count: number) => void;
2223
}
2324

2425
const GraphvizParent: React.FC<GraphvizParentProps> = ({
2526
csvData,
2627
filter,
2728
selfLoops,
2829
minVisits,
30+
onMaxEdgeCountChange
2931
}) => {
3032
const [dotString, setDotString] = useState<string | null>(null);
3133
const [filteredDotString, setFilteredDotString] = useState<string | null>(null);
@@ -52,6 +54,9 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
5254
topSequences
5355
} = countEdges(stepSequences, outcomeSequences);
5456

57+
// Update the maxEdgeCount in the parent component
58+
onMaxEdgeCountChange(maxEdgeCount);
59+
5560
if (JSON.stringify(top5Sequences) !== JSON.stringify(topSequences) || top5Sequences === null) {
5661
setTop5Sequences(topSequences);
5762
if (topSequences && selectedSequence === undefined) {
@@ -89,7 +94,7 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
8994
)
9095
);
9196
}
92-
}, [csvData, selfLoops, minVisits, selectedSequence, setTop5Sequences, top5Sequences]);
97+
}, [csvData, selfLoops, minVisits, selectedSequence, setTop5Sequences, top5Sequences, onMaxEdgeCountChange]);
9398

9499
useEffect(() => {
95100
if (filter) {

src/components/GraphvizProcessing.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -170,35 +170,47 @@ export const countEdges = (
170170
edgeOutcomeCounts: { [p: string]: { [p: string]: number } };
171171
maxEdgeCount: number;
172172
ratioEdges: { [p: string]: number };
173-
edgeCounts: { [p: string]: number };
173+
edgeCounts: { [key: string]: number };
174174
topSequences: SequenceCount[];
175175
} => {
176176
const totalNodeEdges: { [key: string]: number } = {};
177177
const edgeOutcomeCounts: { [key: string]: { [outcome: string]: number } } = {};
178178
let maxEdgeCount = 0;
179179
const ratioEdges: { [key: string]: number } = {};
180180
const edgeCounts: { [key: string]: number } = {};
181+
const studentEdgeCounts: { [key: string]: Set<string> } = {}; // Track unique students per edge
181182
const top5Sequences = getTopSequences(stepSequences, 5);
182183

183-
// Iterate over first-level keys (e.g., student ID, problem, etc.)
184+
// Iterate over first-level keys (student IDs)
184185
Object.keys(stepSequences).forEach((studentId) => {
185-
const innerStepSequences = stepSequences[studentId]; // { [key: string]: string[] }
186-
const innerOutcomeSequences = outcomeSequences[studentId] || {}; // Handle missing outcome sequences
186+
const innerStepSequences = stepSequences[studentId];
187+
const innerOutcomeSequences = outcomeSequences[studentId] || {};
187188

188-
// Iterate over second-level keys (actual step sequences)
189+
// Iterate over second-level keys (problem names)
189190
Object.keys(innerStepSequences).forEach((problemName) => {
190-
const steps = innerStepSequences[problemName]; // string[]
191-
const outcomes = innerOutcomeSequences[problemName] || []; // string[] (fallback to empty array)
191+
const steps = innerStepSequences[problemName];
192+
const outcomes = innerOutcomeSequences[problemName] || [];
192193

193-
if (steps.length < 2) return; // Ignore sequences with < 2 steps
194+
if (steps.length < 2) return;
194195

195196
for (let i = 0; i < steps.length - 1; i++) {
196197
const currentStep = steps[i];
197198
const nextStep = steps[i + 1];
198199
const outcome = outcomes[i + 1];
199200

200201
const edgeKey = `${currentStep}->${nextStep}`;
201-
edgeCounts[edgeKey] = (edgeCounts[edgeKey] || 0) + 1;
202+
203+
// Initialize the Set for this edge if it doesn't exist
204+
if (!studentEdgeCounts[edgeKey]) {
205+
studentEdgeCounts[edgeKey] = new Set();
206+
}
207+
208+
// Add this student to the Set for this edge
209+
studentEdgeCounts[edgeKey].add(studentId);
210+
211+
// Update edge counts based on unique students
212+
edgeCounts[edgeKey] = studentEdgeCounts[edgeKey].size;
213+
202214
edgeOutcomeCounts[edgeKey] = edgeOutcomeCounts[edgeKey] || {};
203215
edgeOutcomeCounts[edgeKey][outcome] = (edgeOutcomeCounts[edgeKey][outcome] || 0) + 1;
204216
totalNodeEdges[currentStep] = (totalNodeEdges[currentStep] || 0) + 1;
@@ -213,7 +225,7 @@ export const countEdges = (
213225
// Compute ratioEdges based on totalNodeEdges
214226
Object.keys(edgeCounts).forEach((edge) => {
215227
const [start] = edge.split('->');
216-
ratioEdges[edge] = edgeCounts[edge] / (totalNodeEdges[start] || 1); // Avoid division by zero
228+
ratioEdges[edge] = edgeCounts[edge] / (totalNodeEdges[start] || 1);
217229
});
218230

219231
return {
@@ -377,8 +389,8 @@ function calculateEdgeColors(outcomes: { [outcome: string]: number }): string {
377389

378390
if (edgeCount > min_visits) {
379391
const tooltip = `${currentStep} to ${nextStep}\n`
380-
+ `- Edge Count: \n\t\t ${edgeCount}\n`
381-
+ `- Total Count for ${currentStep}: \n\t\t${totalCount}\n`
392+
+ `- Unique Students: \n\t\t ${edgeCount}\n`
393+
+ `- Total Students at ${currentStep}: \n\t\t${totalNodeEdges[currentStep] || 0}\n`
382394
+ `- Ratio: \n\t\t${((ratioEdges[edgeKey] || 0) * 100).toFixed(2)}% of students at ${currentStep} go to ${nextStep}\n`
383395
+ `- Outcomes: \n\t\t ${Object.entries(outcomes).map(([outcome, count]) => `${outcome}: ${count}`).join('\n\t\t ')}\n`
384396
+ `- Color Codes: \n\t\t Hex: ${color}`;
@@ -411,8 +423,8 @@ function calculateEdgeColors(outcomes: { [outcome: string]: number }): string {
411423

412424
if (edgeCount > min_visits) {
413425
const tooltip = `${currentStep} to ${nextStep}\n`
414-
+ `- Edge Count: \n\t\t ${edgeCount}\n`
415-
+ `- Total Count for ${currentStep}: \n\t\t${totalCount}\n`
426+
+ `- Unique Students: \n\t\t ${edgeCount}\n`
427+
+ `- Total Students at ${currentStep}: \n\t\t${totalNodeEdges[currentStep] || 0}\n`
416428
+ `- Ratio: \n\t\t${((ratioEdges[edge] || 0) * 100).toFixed(2)}% of students at ${currentStep} go to ${nextStep}\n`
417429
+ `- Outcomes: \n\t\t ${outcomesStr}\n`
418430
+ `- 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)]}`;

src/components/slider.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ interface SliderProps {
66
step?: number;
77
value: number;
88
onChange: (value: number) => void;
9+
maxEdgeCount?: number;
910
}
1011

11-
const Slider: React.FC<SliderProps> = ({min, max, step = 1, value, onChange}) => {
12+
const Slider: React.FC<SliderProps> = ({min, max, step = 1, value, onChange, maxEdgeCount = 100}) => {
1213
const handleSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
1314
const newValue = Number(event.target.value);
1415
onChange(newValue);
@@ -21,10 +22,13 @@ const Slider: React.FC<SliderProps> = ({min, max, step = 1, value, onChange}) =>
2122
}
2223
};
2324

25+
// Calculate actual number of students from percentage
26+
const actualStudents = Math.round((value / 100) * maxEdgeCount);
27+
2428
return (
2529
<div className="space-y-2">
2630
<label className="block text-sm font-medium text-gray-700">
27-
Minimum Edge Visits: {value}
31+
Minimum Edge Visits: {value}% ({actualStudents} students)
2832
</label>
2933
<div className="flex items-center space-x-4">
3034
<input

0 commit comments

Comments
 (0)