Skip to content

Commit 2c61f7e

Browse files
committed
Major Features:
- Replace global min visits slider with per-graph controls via GraphMenu - Convert filter from single select to multi-checkbox for simultaneous filters - Add progressive filtering for Selected Sequence graph via checkbox - Optimize graph container widths (Selected Sequence: 350px, others: 475/575px) UI/UX Improvements: - Move Reset button from header to Properties section - Fix Selected Sequence title alignment and button positioning - Align graph reset button (↻) with GraphMenu hamburger icon - Make graph containers wider to improve visibility Component Changes: - Create GraphMenu component with collapsible min visits slider - Create GraphMinVisitsSlider component for per-graph threshold control - Create SequenceFilterCheckbox for progressive vs all students filtering - Add Checkbox UI component for multi-select filters Graph Processing: - Fix transition probability calculation for Selected Sequence progressive mode - Track students at each sequence position for accurate ratio calculations - Support multiple simultaneous filter graphs - Add problemName to export filenames for better file organization Code Structure: - Remove global slider and input components from App.tsx - Refactor FilterComponent to use checkboxes instead of dropdown - Update GraphvizParent to handle multiple filters and per-graph settings - Clean up unused imports and dead code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9a6a7e3 commit 2c61f7e

File tree

4 files changed

+573
-276
lines changed

4 files changed

+573
-276
lines changed

src/App.tsx

Lines changed: 25 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@ import {Button} from './components/ui/button';
44
import Upload from "@/components/Upload.tsx";
55
import GraphvizParent from "@/components/GraphvizParent.tsx";
66
import FilterComponent from './components/FilterComponent.tsx';
7-
import Slider from '@/components/slider.tsx';
87
import SequenceSelector from "@/components/SequenceSelector.tsx";
98
import {Context, SequenceCount} from "@/Context.tsx";
109
import {
1110
Popover,
1211
PopoverContent,
1312
PopoverTrigger,
1413
} from "@/components/ui/popover"
15-
import {Input} from "@/components/ui/input"
1614

1715
import Loading from './components/Loading.tsx';
1816
import Switch from "./components/switch.tsx";
@@ -69,8 +67,8 @@ const getFileTypeIcon = (filename: string): string => {
6967
function App() {
7068
// State to hold the uploaded CSV data as a string
7169
// const [csvData, setCsvData] = useState<string>('');
72-
// State to manage the filter value for filtering the graph data
73-
const [filter, setFilter] = useState<string>('');
70+
// State to manage the filter values for filtering the graph data (can show multiple)
71+
const [filters, setFilters] = useState<string[]>([]);
7472
// State to toggle whether self-loops (transitions back to the same node) should be included
7573
const [selfLoops, setSelfLoops] = useState<boolean>(true);
7674
const [errorMode, setErrorMode] = useState<boolean>(false);
@@ -178,16 +176,6 @@ function App() {
178176
*/
179177
const handleToggleUniqueStudentMode = () => setUniqueStudentMode(!uniqueStudentMode);
180178

181-
182-
/**
183-
* Updates the minimum visits for edges in the graph when the slider is moved.
184-
*
185-
* @param {number} value - The new value for minimum visits.
186-
*/
187-
const handleSlider = (value: number) => {
188-
setMinVisitsPercentage(value);
189-
};
190-
191179
/**
192180
* Updates the `csvData` state with the uploaded CSV data when the file is processed.
193181
*
@@ -202,22 +190,9 @@ function App() {
202190
}
203191
};
204192

205-
// Calculate actual min visits from percentage
193+
// Calculate actual min visits from percentage (still needed for GraphvizParent)
206194
const minVisits = Math.round((minVisitsPercentage / 100) * maxEdgeCount);
207195

208-
/**
209-
* Updates the minimum visits percentage when the input value changes.
210-
*
211-
* @param {string} value - The new value from the input.
212-
*/
213-
const handleInputChange = (value: string) => {
214-
const numValue = parseInt(value);
215-
if (!isNaN(numValue)) {
216-
const percentage = Math.min(100, Math.max(0, (numValue / maxEdgeCount) * 100));
217-
setMinVisitsPercentage(percentage);
218-
}
219-
};
220-
221196
/**
222197
* Updates the loading state when the file upload or processing begins or ends.
223198
*
@@ -230,17 +205,6 @@ function App() {
230205
<header className="bg-white shadow-sm border-b border-gray-200 px-4 py-3 mb-4">
231206
<div className="max-w-7xl mx-auto flex items-center justify-between">
232207
<h1 className="text-2xl text-gray-900">Path Analysis Tool</h1>
233-
<div className="flex items-center space-x-4">
234-
<Button
235-
variant="destructive"
236-
onClick={() => {
237-
resetData();
238-
setFileInfo(null);
239-
}}
240-
>
241-
Reset
242-
</Button>
243-
</div>
244208
</div>
245209
</header>
246210
{!showControls && <Upload onDataProcessed={handleDataProcessed}/>}
@@ -260,8 +224,8 @@ function App() {
260224
showControls && (
261225
<div className="p-5 m-2 flex flex-col gap-3">
262226

263-
<div className="selected-sequence-bar flex justify-between bg-gray-200 p-4 mb-4">
264-
<h2 className="text-lg font-semibold">Selected Sequence:</h2>
227+
<div className="selected-sequence-bar flex items-center bg-gray-200 p-4 mb-4">
228+
<h2 className="text-lg font-semibold whitespace-nowrap">Selected Sequence:</h2>
265229
{selectedSequence && (
266230
<h2 className="flex-1 text-sm break-words whitespace-normal ml-2">
267231
{selectedSequence.toString().split(',').join(' → ')}
@@ -290,16 +254,17 @@ function App() {
290254
</div>
291255
</div>
292256
)}
293-
{/* Properties Button */}
294-
<Popover>
295-
<PopoverTrigger
296-
className="w-fit bg-slate-500 p-3 rounded-lg text-white">Properties</PopoverTrigger>
297-
<PopoverContent className="w-96 bg-white rounded-lg shadow-lg p-6 border border-gray-200 mx-10">
257+
{/* Properties and Reset Buttons */}
258+
<div className="flex items-center gap-2">
259+
<Popover>
260+
<PopoverTrigger
261+
className="w-fit bg-slate-500 p-3 rounded-lg text-white">Properties</PopoverTrigger>
262+
<PopoverContent className="w-96 bg-white rounded-lg shadow-lg p-6 border border-gray-200 mx-10">
298263
<div className="flex flex-col space-y-6">
299264
{/* Filter Section */}
300265
<div className="space-y-2">
301266
<h3 className="text-lg font-semibold text-gray-900">Filters</h3>
302-
<FilterComponent onFilterChange={setFilter} currentFilter={filter}/>
267+
<FilterComponent onFilterChange={setFilters} currentFilters={filters}/>
303268
</div>
304269

305270
{/* Sequence Section */}
@@ -340,62 +305,22 @@ function App() {
340305
</label>
341306
<Switch isOn={uniqueStudentMode} handleToggle={handleToggleUniqueStudentMode}/>
342307
</div>
343-
344-
345-
<div className="space-y-2">
346-
<label className="text-sm font-medium text-gray-700">
347-
{uniqueStudentMode ? 'Minimum Students' : 'Minimum Visits'}
348-
</label>
349-
<div className="space-y-4">
350-
<div>
351-
<div className="flex justify-between mb-1">
352-
{/* <span className="text-sm text-gray-500">Percentage</span>
353-
<span className="text-sm text-gray-500">{minVisitsPercentage}%</span> */}
354-
</div>
355-
<div className="flex items-center gap-2">
356-
<Slider
357-
step={1}
358-
min={0}
359-
max={maxMinEdgeCount > 0 ? Math.round((maxMinEdgeCount / maxEdgeCount) * 100) : 100}
360-
value={minVisitsPercentage}
361-
onChange={handleSlider}
362-
maxEdgeCount={maxEdgeCount}
363-
uniqueStudentMode={uniqueStudentMode}
364-
/>
365-
<span className="text-sm text-gray-500">%</span>
366-
</div>
367-
</div>
368-
<div>
369-
<div className="flex justify-between mb-1">
370-
<span className="text-sm text-gray-500">
371-
{uniqueStudentMode ? 'Students' : 'Visits'}
372-
</span>
373-
</div>
374-
<Input
375-
type="number"
376-
value={minVisits}
377-
onChange={(e) => handleInputChange(e.target.value)}
378-
className="w-full"
379-
min={0}
380-
max={maxEdgeCount}
381-
/>
382-
</div>
383-
</div>
384-
<div className="space-y-1">
385-
<p className="text-xs text-gray-500">
386-
Controls the "All Students, All Paths" graph visibility
387-
</p>
388-
<p className="text-xs text-gray-500">
389-
Maximum threshold before any node becomes
390-
disconnected: {maxMinEdgeCount} {uniqueStudentMode ? 'students' : 'visits'}
391-
</p>
392-
</div>
393-
</div>
394308
</div>
395309
</div>
396310
</PopoverContent>
397311
</Popover>
398312

313+
<Button
314+
variant="destructive"
315+
onClick={() => {
316+
resetData();
317+
setFileInfo(null);
318+
}}
319+
className="p-3"
320+
>
321+
Reset
322+
</Button>
323+
</div>
399324

400325
{/* Graph and Data Display */}
401326
{!loading && csvData && (
@@ -405,13 +330,14 @@ function App() {
405330
{/* GraphvizParent component generates and displays the graph based on the CSV data */}
406331
<GraphvizParent
407332
csvData={csvData}
408-
filter={filter}
333+
filters={filters}
409334
selfLoops={uniqueStudentMode ? false : selfLoops}
410335
minVisits={minVisits}
411336
onMaxEdgeCountChange={setMaxEdgeCount}
412337
onMaxMinEdgeCountChange={setMaxMinEdgeCount}
413338
errorMode={errorMode}
414339
uniqueStudentMode={uniqueStudentMode}
340+
problemName={fileInfo?.filename.replace(/\.(csv|CSV)$/, '') || 'unknown'}
415341
/>
416342
</div>
417343
</div>

src/components/FilterComponent.tsx

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,46 @@
11
import React from 'react';
2-
import {
3-
Select,
4-
SelectContent,
5-
SelectGroup,
6-
SelectItem,
7-
SelectTrigger,
8-
SelectValue,
9-
} from "@/components/ui/select"
2+
import { Checkbox } from "@/components/ui/checkbox"
3+
import { Label } from "@/components/ui/label"
104

115
interface FilterComponentProps {
12-
onFilterChange: (filter: string) => void;
13-
currentFilter: string;
6+
onFilterChange: (filters: string[]) => void;
7+
currentFilters: string[];
148
}
159

16-
const FilterComponent: React.FC<FilterComponentProps> = ({ onFilterChange, currentFilter }) => {
10+
const FilterComponent: React.FC<FilterComponentProps> = ({ onFilterChange, currentFilters }) => {
11+
const handleCheckboxChange = (value: string, checked: boolean) => {
12+
if (checked) {
13+
// Add the filter if it's not already in the array
14+
if (!currentFilters.includes(value)) {
15+
onFilterChange([...currentFilters, value]);
16+
}
17+
} else {
18+
// Remove the filter
19+
onFilterChange(currentFilters.filter(f => f !== value));
20+
}
21+
};
22+
1723
return (
18-
<div className="space-y-2">
19-
<Select value={currentFilter} onValueChange={(value) => onFilterChange(value)}>
20-
<SelectTrigger>
21-
<SelectValue placeholder="Filter by Status" />
22-
</SelectTrigger>
23-
<SelectContent>
24-
<SelectGroup>
25-
<SelectItem value="ALL">All Statuses</SelectItem>
26-
<SelectItem value="GRADUATED">Graduated</SelectItem>
27-
<SelectItem value="PROMOTED">Promoted</SelectItem>
28-
</SelectGroup>
29-
</SelectContent>
30-
</Select>
24+
<div className="space-y-3">
25+
<Label className="text-sm font-medium">Filter by Status:</Label>
26+
<div className="space-y-2">
27+
<div className="flex items-center space-x-2">
28+
<Checkbox
29+
id="graduated"
30+
checked={currentFilters.includes('GRADUATED')}
31+
onCheckedChange={(checked) => handleCheckboxChange('GRADUATED', checked as boolean)}
32+
/>
33+
<Label htmlFor="graduated" className="cursor-pointer">Graduated</Label>
34+
</div>
35+
<div className="flex items-center space-x-2">
36+
<Checkbox
37+
id="promoted"
38+
checked={currentFilters.includes('PROMOTED')}
39+
onCheckedChange={(checked) => handleCheckboxChange('PROMOTED', checked as boolean)}
40+
/>
41+
<Label htmlFor="promoted" className="cursor-pointer">Promoted</Label>
42+
</div>
43+
</div>
3144
</div>
3245
);
3346
};

0 commit comments

Comments
 (0)