+
+ 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
+