Subjects
{anyHidden && (
)}
{groups.map(({ group, subjects }) => {
const ids = subjects.map((s) => s.id);
const state = groupVisibilityState(ids, hiddenSubjects);
return (
);
})}
);
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `cd frontend && npx jest tests/SubjectSidebar.test.jsx`
Expected: PASS — all tests green.
- [ ] **Step 5: Commit**
```bash
git add frontend/src/components/SubjectSidebar.jsx frontend/tests/SubjectSidebar.test.jsx
git commit -m "feat(frontend): add SubjectSidebar visibility panel"
```
---
### Task 3: Wire state, sidebar, and behaviors into ExperimentAnalysisCharts
**Files:**
- Modify: `frontend/src/components/AnalysisCharts.jsx` (`ExperimentAnalysisCharts`, lines ~446–641; imports at top)
- Test: `frontend/tests/ExperimentAnalysisCharts.test.jsx` (create)
- [ ] **Step 1: Add imports**
At the top of `frontend/src/components/AnalysisCharts.jsx`, add after the existing `date-fns` import:
```jsx
import { SubjectSidebar } from './SubjectSidebar';
import { sortPayloadByValueDesc, toggleInSet, setIdsHidden } from '../lib/crossSubjectChart';
```
- [ ] **Step 2: Add shared state and handlers**
In `ExperimentAnalysisCharts`, immediately after the existing `const [tickStep, setTickStep] = useState(1);` line (~454), add:
```jsx
// Cross-subject interaction state (client-only; global across both charts)
const [sidebarOpen, setSidebarOpen] = useState(false);
const [hiddenSubjects, setHiddenSubjects] = useState(() => new Set());
const [highlightedSubject, setHighlightedSubject] = useState(null);
const toggleHidden = (id) => setHiddenSubjects((prev) => toggleInSet(prev, id));
const toggleGroup = (ids, hidden) => setHiddenSubjects((prev) => setIdsHidden(prev, ids, hidden));
const showAll = () => setHiddenSubjects(new Set());
```
- [ ] **Step 3: Update the `ExpTooltip` to order rows by value descending**
Replace the existing `ExpTooltip` function (~518–533) with:
```jsx
function ExpTooltip({ active, payload, label, unit = '' }) {
if (!active || !payload?.length) return null;
const rows = sortPayloadByValueDesc(payload);
return (
{label}
{rows.map((p) => {
const line = lines.find((l) => l.id === p.dataKey);
return (
{line?.name ?? p.dataKey}{line?.group && line.group !== '—' ? ` (${line.group})` : ''}: {p.value}{unit}
);
})}
);
}
```
- [ ] **Step 4: Add the sidebar toggle button to the global controls row**
In the global-controls `` block, immediately after the `
` line (~556), add:
```jsx
{hasData && (
)}
```
Note: `hasData` is declared at ~509 (`const hasData = lines.length > 0;`), before the `return`, so it is in scope here.
- [ ] **Step 5: Wrap the charts in a flex row with the sidebar**
Replace the `{hasData && (` fragment opener and its closing for the charts block. The current structure is:
```jsx
{hasData && (
<>
{/* Chart 1 — counts */}
...
{/* Chart 2 — success rate */}
...
>
)}
```
Change ONLY the wrapper (keep both chart `
` blocks exactly as they are between the markers):
```jsx
{hasData && (
{/* Chart 1 — counts */}
{/* ...unchanged chart 1
... */}
{/* Chart 2 — success rate */}
{/* ...unchanged chart 2
... */}
{sidebarOpen && (
)}
)}
```
`min-w-0` on the chart column lets the flex child shrink so `ResponsiveContainer` re-flows when the sidebar is open.
- [ ] **Step 6: Add legend handlers and per-line hide/highlight to BOTH charts**
In the **counts** chart, replace the `
` (~602–603) with:
```jsx