Skip to content

Commit fe70a4e

Browse files
committed
feat: Add unique student mode toggle and improve UI components
- Add toggle mode for switching between unique students (first attempts only) and total visits (all attempts) for graph creation - Update App.tsx with new uniqueStudentMode state and toggle handler - Modify GraphvizParent.tsx to handle both counting modes in graph generation and calculations - Update GraphvizProcessing.ts generateDotString function to support both data sources - Fix calculateMaxMinEdgeCount function signature and calls to properly handle both modes - Make menu text dynamic: "Minimum Students" vs "Minimum Visits" based on selected mode - Update slider component to display appropriate labels for each mode - Enhance export button styling with download icon, hover effects, and better visual design - Fix filtered graph alignment by standardizing header margin with other graphs 🤖 Generated with [Claude Code](https://claude.ai/code)
1 parent d328ef0 commit fe70a4e

File tree

4 files changed

+211
-56
lines changed

4 files changed

+211
-56
lines changed

src/App.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ function App() {
2525
const [filter, setFilter] = useState<string>('');
2626
// State to toggle whether self-loops (transitions back to the same node) should be included
2727
const [selfLoops, setSelfLoops] = useState<boolean>(true);
28-
const [errorMode, setErrorMode] = useState<boolean>(false)
28+
const [errorMode, setErrorMode] = useState<boolean>(false);
29+
const [uniqueStudentMode, setUniqueStudentMode] = useState<boolean>(false);
2930
// State to manage the minimum number of visits for displaying edges in the graph
3031
const [minVisitsPercentage, setMinVisitsPercentage] = useState<number>(0);
3132
const {
@@ -46,8 +47,11 @@ function App() {
4647

4748
// Update minVisitsPercentage when maxMinEdgeCount changes
4849
useEffect(() => {
50+
console.log("App.tsx: maxMinEdgeCount changed to:", maxMinEdgeCount);
51+
console.log("App.tsx: maxEdgeCount is:", maxEdgeCount);
4952
if (maxMinEdgeCount > 0) {
5053
const percentage = (maxMinEdgeCount / maxEdgeCount) * 100;
54+
console.log("App.tsx: Setting slider to percentage:", percentage);
5155
setMinVisitsPercentage(Math.max(0, Math.min(100, percentage)));
5256
}
5357
}, [maxMinEdgeCount, maxEdgeCount]);
@@ -102,10 +106,15 @@ function App() {
102106
const handleToggle = () => setSelfLoops(!selfLoops);
103107

104108
/**
105-
* Toggles the self-loops inclusion in the graph by switching the state.
109+
* Toggles the error mode inclusion in the graph by switching the state.
106110
*/
107111
const handleToggleError = () => setErrorMode(!errorMode);
108112

113+
/**
114+
* Toggles between unique students (first attempts only) and total visits (all attempts) mode.
115+
*/
116+
const handleToggleUniqueStudentMode = () => setUniqueStudentMode(!uniqueStudentMode);
117+
109118
/**
110119
* Updates the minimum visits for edges in the graph when the slider is moved.
111120
*
@@ -220,8 +229,17 @@ function App() {
220229
<Switch isOn={errorMode} handleToggle={handleToggleError}/>
221230
</div>
222231

232+
<div className="pb-2 border-b border-gray-200">
233+
<label className="text-sm font-medium text-gray-700">
234+
{uniqueStudentMode ? 'Unique Students Only (First Attempts)' : 'Total Visits (All Attempts)'}
235+
</label>
236+
<Switch isOn={uniqueStudentMode} handleToggle={handleToggleUniqueStudentMode}/>
237+
</div>
238+
223239
<div className="space-y-2">
224-
<label className="text-sm font-medium text-gray-700">Minimum Students</label>
240+
<label className="text-sm font-medium text-gray-700">
241+
{uniqueStudentMode ? 'Minimum Students' : 'Minimum Visits'}
242+
</label>
225243
<div className="space-y-4">
226244
<div>
227245
<div className="flex justify-between mb-1">
@@ -236,13 +254,16 @@ function App() {
236254
value={minVisitsPercentage}
237255
onChange={handleSlider}
238256
maxEdgeCount={maxEdgeCount}
257+
uniqueStudentMode={uniqueStudentMode}
239258
/>
240259
<span className="text-sm text-gray-500">%</span>
241260
</div>
242261
</div>
243262
<div>
244263
<div className="flex justify-between mb-1">
245-
<span className="text-sm text-gray-500">Students</span>
264+
<span className="text-sm text-gray-500">
265+
{uniqueStudentMode ? 'Students' : 'Visits'}
266+
</span>
246267
</div>
247268
<Input
248269
type="number"
@@ -256,7 +277,7 @@ function App() {
256277
</div>
257278
<p className="text-xs text-gray-500">
258279
Maximum threshold before any node becomes
259-
disconnected: {maxMinEdgeCount} students
280+
disconnected: {maxMinEdgeCount} {uniqueStudentMode ? 'students' : 'visits'}
260281
</p>
261282
</div>
262283
</div>
@@ -279,6 +300,7 @@ function App() {
279300
onMaxEdgeCountChange={setMaxEdgeCount}
280301
onMaxMinEdgeCountChange={setMaxMinEdgeCount}
281302
errorMode={errorMode}
303+
uniqueStudentMode={uniqueStudentMode}
282304
/>
283305
</div>
284306
</div>

src/components/GraphvizParent.tsx

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import ErrorBoundary from "@/components/errorBoundary.tsx";
1616
import '../GraphvizContainer.css';
1717
import { Context } from "@/Context.tsx";
1818
import { Button } from './ui/button';
19+
import { Download } from 'lucide-react';
1920

2021
// History item interface
2122
interface HistoryItem {
@@ -38,6 +39,7 @@ interface GraphvizParentProps {
3839
onMaxEdgeCountChange: (count: number) => void;
3940
onMaxMinEdgeCountChange: (count: number) => void;
4041
errorMode: boolean;
42+
uniqueStudentMode: boolean;
4143
}
4244

4345
const GraphvizParent: React.FC<GraphvizParentProps> = ({
@@ -47,7 +49,8 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
4749
minVisits,
4850
onMaxEdgeCountChange,
4951
onMaxMinEdgeCountChange,
50-
errorMode
52+
errorMode,
53+
uniqueStudentMode
5154
}) => {
5255
const [dotString, setDotString] = useState<string | null>(null);
5356
const [filteredDotString, setFilteredDotString] = useState<string | null>(null);
@@ -110,7 +113,15 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
110113
// Calculate and update the maximum minimum-edge count
111114
const sequenceToUse = selectedSequence || topSequences[0]?.sequence;
112115
if (sequenceToUse) {
113-
const maxMinEdgeCount = calculateMaxMinEdgeCount(newEdgeCounts, sequenceToUse);
116+
console.log("GraphvizParent: Calculating maxMinEdgeCount for MAIN graph");
117+
console.log("GraphvizParent: Sequence length:", sequenceToUse.length);
118+
console.log("GraphvizParent: Total edge count keys:", Object.keys(newEdgeCounts).length);
119+
console.log("GraphvizParent: Unique student mode:", uniqueStudentMode);
120+
121+
// Use edgeCounts (unique students) or totalVisits based on mode
122+
const countsToUse = uniqueStudentMode ? newEdgeCounts : totalVisits;
123+
const maxMinEdgeCount = calculateMaxMinEdgeCount(countsToUse, sequenceToUse);
124+
console.log("GraphvizParent: Setting maxMinEdgeCount to:", maxMinEdgeCount);
114125
onMaxMinEdgeCountChange(maxMinEdgeCount);
115126
}
116127

@@ -121,7 +132,13 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
121132
}
122133
}
123134

124-
const normalizedThicknesses = normalizeThicknesses(newEdgeCounts, maxEdgeCount, 10);
135+
// Use appropriate data for thickness normalization based on mode
136+
const countsForThickness = uniqueStudentMode ? newEdgeCounts : totalVisits;
137+
const maxCountForThickness = uniqueStudentMode ? maxEdgeCount : Math.max(...Object.values(totalVisits), 1);
138+
const normalizedThicknesses = normalizeThicknesses(countsForThickness, maxCountForThickness, 10);
139+
140+
console.log("GraphvizParent: Using counts for thickness:", uniqueStudentMode ? "unique students" : "total visits");
141+
console.log("GraphvizParent: Max count for thickness:", maxCountForThickness);
125142

126143
const dotString = generateDotString(
127144
normalizedThicknesses,
@@ -136,7 +153,8 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
136153
totalVisits,
137154
repeatVisits,
138155
errorMode,
139-
mainGraphData.firstAttemptOutcomes
156+
mainGraphData.firstAttemptOutcomes,
157+
uniqueStudentMode
140158
);
141159

142160
setDotString(dotString);
@@ -155,11 +173,12 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
155173
totalVisits,
156174
repeatVisits,
157175
errorMode,
158-
mainGraphData.firstAttemptOutcomes
176+
mainGraphData.firstAttemptOutcomes,
177+
uniqueStudentMode
159178
)
160179
);
161180
}
162-
}, [mainGraphData, minVisits, selectedSequence, setTop5Sequences, top5Sequences, onMaxEdgeCountChange, onMaxMinEdgeCountChange, errorMode]);
181+
}, [mainGraphData, minVisits, selectedSequence, setTop5Sequences, top5Sequences, onMaxEdgeCountChange, onMaxMinEdgeCountChange, errorMode, uniqueStudentMode]);
163182

164183
// Memoized filtered graph data
165184
const filteredGraphData = useMemo(() => {
@@ -202,11 +221,20 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
202221
// Calculate max min edge count for filtered data
203222
const sequenceToUse = selectedSequence || top5Sequences?.[0]?.sequence;
204223
if (sequenceToUse) {
205-
const filteredMinEdgeCount = calculateMaxMinEdgeCount(filteredEdgeCounts, sequenceToUse);
224+
console.log("GraphvizParent: Calculating maxMinEdgeCount for FILTERED graph");
225+
console.log("GraphvizParent: Filter:", filter);
226+
console.log("GraphvizParent: Filtered edge count keys:", Object.keys(filteredEdgeCounts).length);
227+
console.log("GraphvizParent: Filtered unique student mode:", uniqueStudentMode);
228+
const filteredCountsToUse = uniqueStudentMode ? filteredEdgeCounts : filteredTotalVisits;
229+
const filteredMinEdgeCount = calculateMaxMinEdgeCount(filteredCountsToUse, sequenceToUse);
230+
console.log("GraphvizParent: Setting filtered maxMinEdgeCount to:", filteredMinEdgeCount);
206231
onMaxMinEdgeCountChange(filteredMinEdgeCount);
207232
}
208233

209-
const normalizedThicknesses = normalizeThicknesses(filteredEdgeCounts, filteredMaxEdgeCount, 10);
234+
// Use appropriate data for filtered thickness normalization based on mode
235+
const filteredCountsForThickness = uniqueStudentMode ? filteredEdgeCounts : filteredTotalVisits;
236+
const filteredMaxCountForThickness = uniqueStudentMode ? filteredMaxEdgeCount : Math.max(...Object.values(filteredTotalVisits), 1);
237+
const normalizedThicknesses = normalizeThicknesses(filteredCountsForThickness, filteredMaxCountForThickness, 10);
210238

211239
const filteredDotString = generateDotString(
212240
normalizedThicknesses,
@@ -221,7 +249,8 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
221249
filteredTotalVisits,
222250
filteredRepeatVisits,
223251
errorMode,
224-
filteredGraphData.firstAttemptOutcomes
252+
filteredGraphData.firstAttemptOutcomes,
253+
uniqueStudentMode
225254
);
226255

227256
setFilteredDotString(filteredDotString);
@@ -231,12 +260,13 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
231260
if (mainGraphData) {
232261
const sequenceToUse = selectedSequence || top5Sequences?.[0]?.sequence;
233262
if (sequenceToUse) {
234-
const maxMinEdgeCount = calculateMaxMinEdgeCount(mainGraphData.edgeCounts, sequenceToUse);
263+
const resetCountsToUse = uniqueStudentMode ? mainGraphData.edgeCounts : mainGraphData.totalVisits;
264+
const maxMinEdgeCount = calculateMaxMinEdgeCount(resetCountsToUse, sequenceToUse);
235265
onMaxMinEdgeCountChange(maxMinEdgeCount);
236266
}
237267
}
238268
}
239-
}, [filteredGraphData, minVisits, selectedSequence, top5Sequences, errorMode, mainGraphData, onMaxMinEdgeCountChange]);
269+
}, [filteredGraphData, minVisits, selectedSequence, top5Sequences, errorMode, mainGraphData, onMaxMinEdgeCountChange, uniqueStudentMode]);
240270

241271
// Cleanup all event listeners when component unmounts
242272
useEffect(() => {
@@ -1210,7 +1240,7 @@ const GraphvizParent: React.FC<GraphvizParentProps> = ({
12101240
{filter && filter !== 'ALL' && filteredDotString && (
12111241
<div
12121242
className={`graph-item flex flex-col items-center ${topDotString && dotString && filteredDotString ? 'w-[400px]' : 'w-[500px]'} border-2 border-gray-700 rounded-lg p-4 bg-gray-100 flex-shrink-0`}>
1213-
<h2 className="text-lg font-semibold text-center mb-4">Filtered Graph: {titleCase(filter)}</h2>
1243+
<h2 className="text-lg font-semibold text-center mb-2">Filtered Graph: {titleCase(filter)}</h2>
12141244
<div ref={graphRefFiltered}
12151245
className="w-full h-[575px] border-2 border-gray-700 rounded-lg p-4 bg-white flex items-center justify-center"></div>
12161246
<ExportButton onClick={() => exportGraphAsPNG(graphRefFiltered, 'filtered_graph')} />
@@ -1301,12 +1331,14 @@ interface ExportButtonProps {
13011331
label?: string;
13021332
}
13031333

1304-
function ExportButton({ onClick, label = "Export Image" }: ExportButtonProps) {
1334+
function ExportButton({ onClick, label = "Export as PNG" }: ExportButtonProps) {
13051335
return (
13061336
<Button
1307-
variant={'secondary'}
1337+
variant={'outline'}
13081338
onClick={onClick}
1339+
className="flex items-center gap-2 hover:bg-blue-50 hover:border-blue-300 transition-all duration-200 shadow-sm"
13091340
>
1341+
<Download className="h-4 w-4" />
13101342
{label}
13111343
</Button>
13121344
);

0 commit comments

Comments
 (0)