Skip to content

Commit 8bf0a89

Browse files
committed
Neuropixel grouping
1 parent eaee8ba commit 8bf0a89

4 files changed

Lines changed: 384 additions & 18 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ apps/probe-viewer/public/data/
33
apps/probe-viewer/public/probes-manifest.json
44
apps/probe-viewer/node_modules/
55
apps/probe-viewer/dist/
6+
apps/probe-viewer/.vite/
67

78
# Build cache
89
.cache/

apps/probe-viewer/src/App.css

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,105 @@
128128
color: #475569;
129129
}
130130

131+
.sidebar-item-variant {
132+
font-weight: 400;
133+
color: #64748b;
134+
font-size: 0.85rem;
135+
}
136+
137+
/* Neuropixels hierarchy grouping */
138+
.sidebar-group {
139+
display: flex;
140+
flex-direction: column;
141+
gap: 0.4rem;
142+
}
143+
144+
.sidebar-group-header {
145+
display: flex;
146+
align-items: center;
147+
gap: 0.5rem;
148+
width: 100%;
149+
padding: 0.45rem 0.4rem;
150+
border: none;
151+
background: transparent;
152+
cursor: pointer;
153+
color: #0f172a;
154+
font-weight: 700;
155+
font-size: 0.95rem;
156+
border-bottom: 1px solid rgba(148, 163, 184, 0.35);
157+
}
158+
159+
.sidebar-group-header:hover {
160+
color: #2563eb;
161+
}
162+
163+
.sidebar-group-caret {
164+
width: 1rem;
165+
color: #64748b;
166+
}
167+
168+
.sidebar-group-title {
169+
flex: 1;
170+
text-align: left;
171+
}
172+
173+
.sidebar-group-count {
174+
font-size: 0.75rem;
175+
font-weight: 600;
176+
color: #475569;
177+
background: rgba(148, 163, 184, 0.25);
178+
border-radius: 999px;
179+
padding: 0.05rem 0.5rem;
180+
}
181+
182+
.sidebar-subgroup {
183+
display: flex;
184+
flex-direction: column;
185+
gap: 0.35rem;
186+
padding-left: 0.6rem;
187+
}
188+
189+
.sidebar-subgroup-header {
190+
display: flex;
191+
align-items: center;
192+
gap: 0.4rem;
193+
width: 100%;
194+
padding: 0.3rem 0.25rem;
195+
border: none;
196+
background: transparent;
197+
cursor: pointer;
198+
}
199+
200+
.sidebar-subgroup-header:hover .sidebar-subgroup-title {
201+
color: #2563eb;
202+
}
203+
204+
.sidebar-subgroup-title {
205+
flex: 1;
206+
text-align: left;
207+
font-size: 0.74rem;
208+
font-weight: 700;
209+
text-transform: uppercase;
210+
letter-spacing: 0.03em;
211+
color: #64748b;
212+
}
213+
214+
.sidebar-subdivision {
215+
display: flex;
216+
flex-direction: column;
217+
gap: 0.35rem;
218+
}
219+
220+
/* Static (non-collapsible) length divider inside the Non-human-primate family. */
221+
.sidebar-subgroup-divider {
222+
margin: 0.3rem 0 0.05rem 0.9rem;
223+
font-size: 0.68rem;
224+
font-weight: 600;
225+
text-transform: uppercase;
226+
letter-spacing: 0.04em;
227+
color: #94a3b8;
228+
}
229+
131230
.sidebar-hint {
132231
font-size: 0.9rem;
133232
color: #64748b;

apps/probe-viewer/src/components/Sidebar.tsx

Lines changed: 124 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { useEffect, useMemo } from "react";
1+
import { useEffect, useMemo, useState } from "react";
22

33
import { useAppStore } from "../state/useAppStore";
4+
import type { ManifestEntry } from "../types/probe";
5+
import { groupNeuropixels, variantLabel } from "../utils/neuropixelsGrouping";
46

57
const MANUFACTURER_DISPLAY_NAMES: Record<string, string> = {
68
cambridgeneurotech: "Cambridge NeuroTech",
@@ -59,6 +61,60 @@ export function Sidebar() {
5961
}
6062
}, [filteredEntries, selectedProbeId, selectProbe]);
6163

64+
// Neuropixels (imec) gets a hierarchy-grouped list (platform -> family);
65+
// every other manufacturer keeps the simple flat list.
66+
const isNeuropixels = selectedManufacturer === "imec";
67+
const groups = useMemo(
68+
() => (isNeuropixels ? groupNeuropixels(filteredEntries) : []),
69+
[isNeuropixels, filteredEntries],
70+
);
71+
72+
// Both levels start collapsed (empty expanded sets). A platform shows its
73+
// families only when expanded; a family shows its probes only when expanded.
74+
const [expandedPlatforms, setExpandedPlatforms] = useState<Set<string>>(
75+
() => new Set(),
76+
);
77+
const [expandedFamilies, setExpandedFamilies] = useState<Set<string>>(
78+
() => new Set(),
79+
);
80+
const togglePlatform = (platform: string) =>
81+
setExpandedPlatforms((prev) => {
82+
const next = new Set(prev);
83+
if (next.has(platform)) next.delete(platform);
84+
else next.add(platform);
85+
return next;
86+
});
87+
const toggleFamily = (key: string) =>
88+
setExpandedFamilies((prev) => {
89+
const next = new Set(prev);
90+
if (next.has(key)) next.delete(key);
91+
else next.add(key);
92+
return next;
93+
});
94+
95+
const renderItem = (entry: ManifestEntry, showVariant: boolean) => (
96+
<button
97+
key={entry.id}
98+
type="button"
99+
className={
100+
entry.id === selectedProbeId
101+
? "sidebar-item sidebar-item--active"
102+
: "sidebar-item"
103+
}
104+
onClick={() => selectProbe(entry.id)}
105+
>
106+
<span className="sidebar-item-name">
107+
{entry.displayName}
108+
{showVariant && variantLabel(entry) ? (
109+
<span className="sidebar-item-variant"> {variantLabel(entry)}</span>
110+
) : null}
111+
</span>
112+
<span className="sidebar-item-meta">
113+
{entry.contactCount} contacts · {entry.shankCount} shanks
114+
</span>
115+
</button>
116+
);
117+
62118
return (
63119
<div className="sidebar">
64120
<header className="sidebar-header">
@@ -110,23 +166,73 @@ export function Sidebar() {
110166
{manifestStatus === "success" && filteredEntries.length === 0 && (
111167
<p className="sidebar-hint">No probes match the current filters.</p>
112168
)}
113-
{filteredEntries.map((entry) => (
114-
<button
115-
key={entry.id}
116-
type="button"
117-
className={
118-
entry.id === selectedProbeId
119-
? "sidebar-item sidebar-item--active"
120-
: "sidebar-item"
121-
}
122-
onClick={() => selectProbe(entry.id)}
123-
>
124-
<span className="sidebar-item-name">{entry.displayName}</span>
125-
<span className="sidebar-item-meta">
126-
{entry.contactCount} contacts · {entry.shankCount} shanks
127-
</span>
128-
</button>
129-
))}
169+
170+
{manifestStatus === "success" &&
171+
!isNeuropixels &&
172+
filteredEntries.map((entry) => renderItem(entry, false))}
173+
174+
{manifestStatus === "success" &&
175+
isNeuropixels &&
176+
groups.map((group) => {
177+
const platformOpen = expandedPlatforms.has(group.platform);
178+
return (
179+
<div className="sidebar-group" key={group.platform}>
180+
<button
181+
type="button"
182+
className="sidebar-group-header"
183+
aria-expanded={platformOpen}
184+
onClick={() => togglePlatform(group.platform)}
185+
>
186+
<span className="sidebar-group-caret">
187+
{platformOpen ? "▾" : "▸"}
188+
</span>
189+
<span className="sidebar-group-title">{group.platform}</span>
190+
<span className="sidebar-group-count">{group.count}</span>
191+
</button>
192+
{platformOpen &&
193+
group.families.map((fam) => {
194+
const familyKey = `${group.platform}||${fam.family}`;
195+
const familyOpen = expandedFamilies.has(familyKey);
196+
return (
197+
<div className="sidebar-subgroup" key={fam.family}>
198+
<button
199+
type="button"
200+
className="sidebar-subgroup-header"
201+
aria-expanded={familyOpen}
202+
onClick={() => toggleFamily(familyKey)}
203+
>
204+
<span className="sidebar-group-caret">
205+
{familyOpen ? "▾" : "▸"}
206+
</span>
207+
<span className="sidebar-subgroup-title">
208+
{fam.family}
209+
</span>
210+
<span className="sidebar-group-count">
211+
{fam.entries.length}
212+
</span>
213+
</button>
214+
{familyOpen &&
215+
fam.subgroups.map((sub) => (
216+
<div
217+
className="sidebar-subdivision"
218+
key={sub.label || "_flat"}
219+
>
220+
{sub.label && (
221+
<p className="sidebar-subgroup-divider">
222+
{sub.label}
223+
</p>
224+
)}
225+
{sub.entries.map((entry) =>
226+
renderItem(entry, true),
227+
)}
228+
</div>
229+
))}
230+
</div>
231+
);
232+
})}
233+
</div>
234+
);
235+
})}
130236
</div>
131237
</div>
132238
);

0 commit comments

Comments
 (0)