Skip to content

Commit 95bb27f

Browse files
eurunuelaclaude
andcommitted
Add interactive component metrics table
- Create ComponentTable.js with full metrics data from TSV file - Display columns: Component, Kappa, Rho, Variance, Ranks, Dice, S/N, Classification, Tags - Sync table selection with scatter plots and pie chart - Auto-scroll table to selected row when selecting from charts - Highlight selected row with classification color (green/red) - Add rounded corners on row highlights and classification badges - Fixed-width classification badges for consistent appearance - Fix ToggleSwitch text color for better readability on colored backgrounds 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 87007f0 commit 95bb27f

5 files changed

Lines changed: 326 additions & 1 deletion

File tree

claude-progress.txt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,44 @@ const blue2white = {
537537

538538
---
539539

540+
## Session 3.3 - Component Table (2025-12-14)
541+
542+
### Goals
543+
- Add interactive component metrics table below the charts
544+
- Synchronize table selection with chart selection
545+
- Display original metrics data from TSV file
546+
547+
### Changes Made
548+
549+
#### New Component: ComponentTable.js
550+
- Displays full metrics data from the original TSV file
551+
- Columns: Component, Kappa, Rho, Variance %, κ Rank, ρ Rank, Dice FT2, Dice FS0, S/N t, Classification, Tags
552+
- Interactive row selection synced with charts
553+
- Auto-scrolls to selected row when component is selected from charts
554+
- Selected row highlighted with classification color (green/red)
555+
- Rounded corners on row highlight and classification badges
556+
- Fixed-width classification badges for consistent appearance
557+
- Sticky header row with scrollable body (max 350px height)
558+
559+
#### ToggleSwitch.js Fix
560+
- Changed selected text color from white to dark gray (#1f2937) for readability
561+
562+
### Features
563+
- Click table row to select component (updates all charts)
564+
- Click chart point/slice to select component (table scrolls to row)
565+
- Classification updates reflect in table in real-time
566+
- Hover effects on rows
567+
- Responsive scrolling for large datasets
568+
569+
### Files Created
570+
- `src/Plots/ComponentTable.js`
571+
572+
### Files Modified
573+
- `src/Plots/Plots.js` - Import and render ComponentTable
574+
- `src/Plots/ToggleSwitch.js` - Fix text color contrast
575+
576+
---
577+
540578
## Template for Future Sessions
541579

542580
```

features.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,45 @@
347347
"Verify component selection still updates displayed PNG"
348348
],
349349
"passes": true
350+
},
351+
{
352+
"id": "display_component_table",
353+
"category": "visualization",
354+
"description": "Display interactive component metrics table with full data from metrics TSV file",
355+
"steps": [
356+
"Load a tedana folder",
357+
"Navigate to the 'ICA' tab",
358+
"Verify component metrics table appears below the charts",
359+
"Verify table shows columns: Component, Kappa, Rho, Variance, Ranks, Dice scores, S/N, Classification, Tags",
360+
"Verify table is scrollable for large datasets"
361+
],
362+
"passes": true
363+
},
364+
{
365+
"id": "table_chart_sync",
366+
"category": "interactivity",
367+
"description": "Synchronize selection between component table and charts",
368+
"steps": [
369+
"Load a tedana folder",
370+
"Click on a row in the component table",
371+
"Verify all charts update to highlight the selected component",
372+
"Click on a point in a scatter plot or pie chart",
373+
"Verify the table auto-scrolls to and highlights the selected row"
374+
],
375+
"passes": true
376+
},
377+
{
378+
"id": "table_classification_update",
379+
"category": "interactivity",
380+
"description": "Update table classification when reclassifying components",
381+
"steps": [
382+
"Load a tedana folder",
383+
"Select a component from the table or charts",
384+
"Use A/R keys or toggle switch to change classification",
385+
"Verify the table row highlight color changes (green for accepted, red for rejected)",
386+
"Verify the classification badge in the table updates"
387+
],
388+
"passes": true
350389
}
351390
]
352391
}

src/Plots/ComponentTable.js

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import React, { useRef, useEffect, useMemo } from "react";
2+
3+
const COLORS = {
4+
accepted: "#86EFAC",
5+
acceptedHover: "#22C55E",
6+
rejected: "#FCA5A5",
7+
rejectedHover: "#EF4444",
8+
};
9+
10+
// Format cell value based on type
11+
function formatValue(value, key) {
12+
if (value === null || value === undefined || value === "") return "—";
13+
if (typeof value === "number") {
14+
// Very small numbers in scientific notation
15+
if (Math.abs(value) < 0.0001 && value !== 0) {
16+
return value.toExponential(2);
17+
}
18+
// Regular numbers with 2-4 decimal places
19+
return value.toFixed(2);
20+
}
21+
return String(value);
22+
}
23+
24+
// Get a human-readable column name
25+
function getColumnLabel(key) {
26+
const labels = {
27+
"Component": "Component",
28+
"kappa": "Kappa",
29+
"rho": "Rho",
30+
"variance explained": "Variance %",
31+
"normalized variance explained": "Norm. Var.",
32+
"countsigFT2": "Sig. FT2",
33+
"countsigFS0": "Sig. FS0",
34+
"dice_FT2": "Dice FT2",
35+
"dice_FS0": "Dice FS0",
36+
"signal-noise_t": "S/N t",
37+
"signal-noise_p": "S/N p",
38+
"optimal sign": "Sign",
39+
"classification": "Classification",
40+
"classification_tags": "Tags",
41+
"kappa rank": "κ Rank",
42+
"rho rank": "ρ Rank",
43+
"rationale": "Rationale",
44+
};
45+
return labels[key] || key;
46+
}
47+
48+
// Columns to display (in order)
49+
const DISPLAY_COLUMNS = [
50+
"Component",
51+
"kappa",
52+
"rho",
53+
"variance explained",
54+
"kappa rank",
55+
"rho rank",
56+
"dice_FT2",
57+
"dice_FS0",
58+
"signal-noise_t",
59+
"classification",
60+
"classification_tags",
61+
];
62+
63+
function ComponentTable({ data, selectedIndex, onRowClick, classifications }) {
64+
const selectedRowRef = useRef(null);
65+
const tableContainerRef = useRef(null);
66+
67+
// Scroll selected row into view when selection changes
68+
useEffect(() => {
69+
if (selectedRowRef.current && tableContainerRef.current) {
70+
const container = tableContainerRef.current;
71+
const row = selectedRowRef.current;
72+
73+
// Calculate if row is visible in the container
74+
const containerRect = container.getBoundingClientRect();
75+
const rowRect = row.getBoundingClientRect();
76+
77+
// Check if row is outside visible area
78+
if (rowRect.top < containerRect.top || rowRect.bottom > containerRect.bottom) {
79+
row.scrollIntoView({
80+
behavior: "smooth",
81+
block: "center",
82+
});
83+
}
84+
}
85+
}, [selectedIndex]);
86+
87+
// Determine which columns exist in the data
88+
const columns = useMemo(() => {
89+
if (!data?.length) return [];
90+
const availableKeys = Object.keys(data[0]);
91+
return DISPLAY_COLUMNS.filter((col) => availableKeys.includes(col));
92+
}, [data]);
93+
94+
if (!data?.length) {
95+
return null;
96+
}
97+
98+
const getClassification = (index) => {
99+
// Use classifications array if provided (for live updates), otherwise fall back to data
100+
if (classifications && classifications[index]) {
101+
return classifications[index];
102+
}
103+
return data[index]?.classification || "rejected";
104+
};
105+
106+
const getRowStyle = (index) => {
107+
return {
108+
cursor: "pointer",
109+
};
110+
};
111+
112+
const getCellStyle = (index, colIndex, totalCols) => {
113+
const isSelected = index === selectedIndex;
114+
const classification = getClassification(index);
115+
const baseColor = classification === "accepted" ? COLORS.accepted : COLORS.rejected;
116+
117+
const isFirst = colIndex === 0;
118+
const isLast = colIndex === totalCols - 1;
119+
120+
return {
121+
backgroundColor: isSelected ? baseColor : "transparent",
122+
transition: "background-color 0.15s ease",
123+
borderTopLeftRadius: isSelected && isFirst ? "8px" : "0",
124+
borderBottomLeftRadius: isSelected && isFirst ? "8px" : "0",
125+
borderTopRightRadius: isSelected && isLast ? "8px" : "0",
126+
borderBottomRightRadius: isSelected && isLast ? "8px" : "0",
127+
};
128+
};
129+
130+
return (
131+
<div style={{ width: "100%", padding: "16px 24px 24px 24px" }}>
132+
<h3 className="text-center text-lg font-semibold text-gray-700 mb-3">
133+
Component Metrics
134+
</h3>
135+
<div
136+
ref={tableContainerRef}
137+
className="rounded-lg border border-gray-200 shadow-sm"
138+
style={{
139+
maxHeight: "350px",
140+
overflowY: "auto",
141+
overflowX: "auto",
142+
margin: "0 8px",
143+
}}
144+
>
145+
<table className="w-full text-sm" style={{ borderCollapse: "separate", borderSpacing: "0" }}>
146+
<thead className="sticky top-0 bg-gray-100 text-gray-700 z-10">
147+
<tr>
148+
{columns.map((col) => (
149+
<th
150+
key={col}
151+
className={`px-3 py-2 font-semibold whitespace-nowrap ${
152+
col === "Component" || col === "classification" || col === "classification_tags"
153+
? "text-left"
154+
: "text-right"
155+
}`}
156+
>
157+
{getColumnLabel(col)}
158+
</th>
159+
))}
160+
</tr>
161+
</thead>
162+
<tbody>
163+
{data.map((row, index) => {
164+
const classification = getClassification(index);
165+
return (
166+
<tr
167+
key={row.Component || index}
168+
ref={index === selectedIndex ? selectedRowRef : null}
169+
onClick={() => onRowClick(index)}
170+
style={getRowStyle(index)}
171+
className="border-t border-gray-100 group"
172+
onMouseEnter={(e) => {
173+
if (index !== selectedIndex) {
174+
const cells = e.currentTarget.querySelectorAll("td");
175+
cells.forEach((cell) => {
176+
cell.style.backgroundColor = "#f3f4f6";
177+
});
178+
}
179+
}}
180+
onMouseLeave={(e) => {
181+
if (index !== selectedIndex) {
182+
const cells = e.currentTarget.querySelectorAll("td");
183+
cells.forEach((cell) => {
184+
cell.style.backgroundColor = "transparent";
185+
});
186+
}
187+
}}
188+
>
189+
{columns.map((col, colIndex) => {
190+
const cellStyle = getCellStyle(index, colIndex, columns.length);
191+
if (col === "classification") {
192+
return (
193+
<td key={col} className="px-3 py-2" style={cellStyle}>
194+
<span
195+
className="text-xs font-medium"
196+
style={{
197+
backgroundColor:
198+
classification === "accepted"
199+
? COLORS.accepted
200+
: COLORS.rejected,
201+
color: "#1f2937",
202+
borderRadius: "6px",
203+
display: "inline-block",
204+
padding: "4px 0",
205+
width: "75px",
206+
textAlign: "center",
207+
boxSizing: "border-box",
208+
}}
209+
>
210+
{classification}
211+
</span>
212+
</td>
213+
);
214+
}
215+
return (
216+
<td
217+
key={col}
218+
style={cellStyle}
219+
className={`px-3 py-2 ${
220+
col === "Component" || col === "classification_tags"
221+
? "font-medium text-gray-900"
222+
: "text-right text-gray-600"
223+
}`}
224+
>
225+
{formatValue(row[col], col)}
226+
</td>
227+
);
228+
})}
229+
</tr>
230+
);
231+
})}
232+
</tbody>
233+
</table>
234+
</div>
235+
</div>
236+
);
237+
}
238+
239+
export default ComponentTable;

src/Plots/Plots.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import PieChart from "./PieChart";
77
import TimeSeries from "./TimeSeries";
88
import FFTSpectrum from "./FFTSpectrum";
99
import BrainViewer from "./BrainViewer";
10+
import ComponentTable from "./ComponentTable";
1011
import { assignColor } from "./PlotUtils";
1112

1213
// Chart dimensions - sized to fit 2x2 in half screen width
@@ -412,6 +413,14 @@ function Plots({ componentData, componentFigures, originalData, mixingMatrix, ni
412413
)}
413414
</div>
414415
</div>
416+
417+
{/* Component table - full width below charts */}
418+
<ComponentTable
419+
data={componentData?.[0] || []}
420+
selectedIndex={selectedIndex}
421+
onRowClick={handlePointClick}
422+
classifications={processedData.map((d) => d.classification)}
423+
/>
415424
</div>
416425
);
417426
}

src/Plots/ToggleSwitch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function ToggleSwitch({ values, selected, colors, handleNewSelection }) {
2929
<label
3030
onClick={() => handleNewSelection(val)}
3131
className="relative z-10 h-8 flex items-center justify-center transition-colors duration-200"
32-
style={{ width: 90, color: selected === val ? "#fff" : "rgba(0,0,0,0.6)", cursor: "pointer" }}
32+
style={{ width: 90, color: selected === val ? "#1f2937" : "rgba(0,0,0,0.6)", cursor: "pointer" }}
3333
>
3434
{titleCase(val)}
3535
</label>

0 commit comments

Comments
 (0)