Skip to content

Commit 0978631

Browse files
committed
experimental search neurodata type
1 parent b5d1361 commit 0978631

File tree

1 file changed

+212
-39
lines changed

1 file changed

+212
-39
lines changed

src/pages/DandiPage/experimentalSearch/ExperimentalSearchPanel.tsx

Lines changed: 212 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { FunctionComponent, useEffect, useState, useMemo } from "react";
2+
import { Chip, Stack } from "@mui/material";
23
import { JobRunnerClient } from "./jobRunnerClient";
34

45
type ExperimentalSearchPanelProps = {
@@ -9,6 +10,7 @@ type DandisetInfo = {
910
id: string;
1011
contactPerson: string;
1112
species: string[];
13+
neurodataTypes: string[];
1214
};
1315

1416
type ContactPersonWithSpecies = {
@@ -25,6 +27,7 @@ type SearchData = {
2527
type Filter = {
2628
contactPerson: string | "<not specified>";
2729
species: string | "<not specified>";
30+
neurodataTypes: string[];
2831
};
2932

3033
export const ExperimentalSearchPanel: FunctionComponent<
@@ -55,55 +58,138 @@ export const ExperimentalSearchPanel: FunctionComponent<
5558
const [filter, setFilter] = useState<Filter>({
5659
contactPerson: "<not specified>",
5760
species: "<not specified>",
61+
neurodataTypes: [],
5862
});
5963
const { searchData } = useSearchData(jobRunnerClient);
6064
const dandisetIds = useFilteredDandisets(searchData, filter);
6165

6266
const availableContactPersons = useMemo(() => {
6367
if (!searchData) return [];
64-
if (filter.species === "<not specified>") {
65-
return searchData.contactPersons;
66-
}
6768

68-
// When a species is selected, show only contact persons who have that species
69-
return searchData.contactPersons
70-
.filter((person) => person.species.includes(filter.species))
71-
.map((person) => ({
72-
...person,
73-
count: 1, // Since we know they have this species
69+
// Filter dandisets based on current criteria
70+
const filteredDandisets = searchData.dandisetInfo.filter((info) => {
71+
if (
72+
filter.species !== "<not specified>" &&
73+
!info.species.includes(filter.species)
74+
) {
75+
return false;
76+
}
77+
if (
78+
filter.neurodataTypes.length > 0 &&
79+
!filter.neurodataTypes.every((type) =>
80+
info.neurodataTypes.includes(type),
81+
)
82+
) {
83+
return false;
84+
}
85+
return true;
86+
});
87+
88+
// Get unique contact persons from filtered dandisets
89+
const contactPersonCounts = new Map<string, number>();
90+
filteredDandisets.forEach((info) => {
91+
contactPersonCounts.set(
92+
info.contactPerson,
93+
(contactPersonCounts.get(info.contactPerson) || 0) + 1,
94+
);
95+
});
96+
97+
// Map to required format
98+
return Array.from(contactPersonCounts.entries())
99+
.map(([name, count]) => ({
100+
name,
101+
species:
102+
searchData.contactPersons.find((p) => p.name === name)?.species || [],
103+
count,
104+
}))
105+
.sort((a, b) => a.name.localeCompare(b.name));
106+
}, [searchData, filter.species, filter.neurodataTypes]);
107+
108+
const availableNeurodataTypes = useMemo(() => {
109+
if (!searchData) return [];
110+
111+
// Filter dandisets based on current criteria except the neurodataTypes we're currently selecting
112+
const filteredDandisets = searchData.dandisetInfo.filter((info) => {
113+
if (
114+
filter.contactPerson !== "<not specified>" &&
115+
info.contactPerson !== filter.contactPerson
116+
) {
117+
return false;
118+
}
119+
if (
120+
filter.species !== "<not specified>" &&
121+
!info.species.includes(filter.species)
122+
) {
123+
return false;
124+
}
125+
// Only consider existing neurodata type selections
126+
if (filter.neurodataTypes.length > 0) {
127+
// A dandiset must have all currently selected types to be considered
128+
const hasAllSelectedTypes = filter.neurodataTypes.every(
129+
(selectedType) => info.neurodataTypes.includes(selectedType),
130+
);
131+
if (!hasAllSelectedTypes) {
132+
return false;
133+
}
134+
}
135+
return true;
136+
});
137+
138+
// Get all unique neurodata types from filtered dandisets
139+
const allTypes = new Set<string>();
140+
filteredDandisets.forEach((dandiset) => {
141+
dandiset.neurodataTypes.forEach((type) => allTypes.add(type));
142+
});
143+
144+
return Array.from(allTypes)
145+
.sort()
146+
.map((name) => ({
147+
name,
148+
count: filteredDandisets.reduce(
149+
(acc, dandiset) =>
150+
acc + (dandiset.neurodataTypes.includes(name) ? 1 : 0),
151+
0,
152+
),
74153
}));
75-
}, [searchData, filter.species]);
154+
}, [searchData, filter.contactPerson, filter.species, filter.neurodataTypes]);
76155

77156
const availableSpecies = useMemo(() => {
78157
if (!searchData) return [];
79-
if (filter.contactPerson === "<not specified>") {
80-
// When no contact person is selected, show all unique species
81-
const allSpecies = new Set<string>();
82-
searchData.contactPersons.forEach((person) => {
83-
person.species.forEach((species) => allSpecies.add(species));
84-
});
85-
return Array.from(allSpecies)
86-
.sort()
87-
.map((name) => ({
88-
name,
89-
count: searchData.contactPersons.reduce(
90-
(acc, person) => acc + (person.species.includes(name) ? 1 : 0),
91-
0,
92-
),
93-
}));
94-
}
95158

96-
// When a contact person is selected, show only their species
97-
const person = searchData.contactPersons.find(
98-
(p) => p.name === filter.contactPerson,
99-
);
100-
if (!person) return [];
159+
// Filter dandisets based on current criteria
160+
const filteredDandisets = searchData.dandisetInfo.filter((info) => {
161+
if (
162+
filter.contactPerson !== "<not specified>" &&
163+
info.contactPerson !== filter.contactPerson
164+
) {
165+
return false;
166+
}
167+
if (
168+
filter.neurodataTypes.length > 0 &&
169+
!filter.neurodataTypes.every((type) =>
170+
info.neurodataTypes.includes(type),
171+
)
172+
) {
173+
return false;
174+
}
175+
return true;
176+
});
101177

102-
return person.species.sort().map((name) => ({
103-
name,
104-
count: 1,
105-
}));
106-
}, [searchData, filter.contactPerson]);
178+
// Get unique species from filtered dandisets
179+
const speciesCounts = new Map<string, number>();
180+
filteredDandisets.forEach((info) => {
181+
info.species.forEach((species) => {
182+
speciesCounts.set(species, (speciesCounts.get(species) || 0) + 1);
183+
});
184+
});
185+
186+
return Array.from(speciesCounts.entries())
187+
.map(([name, count]) => ({
188+
name,
189+
count,
190+
}))
191+
.sort((a, b) => a.name.localeCompare(b.name));
192+
}, [searchData, filter.contactPerson, filter.neurodataTypes]);
107193

108194
// Clear selections if they become unavailable
109195
useEffect(() => {
@@ -129,11 +215,32 @@ export const ExperimentalSearchPanel: FunctionComponent<
129215
setFilter((f) => ({ ...f, contactPerson: "<not specified>" }));
130216
}
131217
}
218+
219+
// Clear neurodata types that are no longer available
220+
if (
221+
filter.neurodataTypes.length > 0 &&
222+
availableNeurodataTypes.length > 0
223+
) {
224+
const availableTypes = new Set(
225+
availableNeurodataTypes.map((t) => t.name),
226+
);
227+
const unavailableTypes = filter.neurodataTypes.filter(
228+
(t) => !availableTypes.has(t),
229+
);
230+
if (unavailableTypes.length > 0) {
231+
setFilter((f) => ({
232+
...f,
233+
neurodataTypes: f.neurodataTypes.filter((t) => availableTypes.has(t)),
234+
}));
235+
}
236+
}
132237
}, [
133238
filter.species,
134239
filter.contactPerson,
240+
filter.neurodataTypes,
135241
availableSpecies,
136242
availableContactPersons,
243+
availableNeurodataTypes,
137244
]);
138245

139246
useEffect(() => {
@@ -203,6 +310,52 @@ export const ExperimentalSearchPanel: FunctionComponent<
203310
</select>
204311
</div>
205312
)}
313+
{searchData && (
314+
<div style={{ marginTop: 20 }}>
315+
<div style={{ marginBottom: 10 }}>Neurodata Types:</div>
316+
<Stack
317+
direction="row"
318+
spacing={1}
319+
sx={{ flexWrap: "wrap", gap: 1, mb: 2 }}
320+
>
321+
{filter.neurodataTypes.map((type) => (
322+
<Chip
323+
key={type}
324+
label={type}
325+
onDelete={() =>
326+
setFilter((f) => ({
327+
...f,
328+
neurodataTypes: f.neurodataTypes.filter((t) => t !== type),
329+
}))
330+
}
331+
/>
332+
))}
333+
</Stack>
334+
<select
335+
value=""
336+
onChange={(e) => {
337+
if (e.target.value) {
338+
setFilter((f) => ({
339+
...f,
340+
neurodataTypes: [...f.neurodataTypes, e.target.value],
341+
}));
342+
e.target.value = ""; // Reset select after adding
343+
}
344+
}}
345+
style={{ width: 400, height: 30 }}
346+
>
347+
<option value="">Add neurodata type...</option>
348+
{availableNeurodataTypes
349+
.filter((type) => !filter.neurodataTypes.includes(type.name))
350+
.map((type) => (
351+
<option key={type.name} value={type.name}>
352+
{type.name} ({type.count} dandiset
353+
{type.count !== 1 ? "s" : ""})
354+
</option>
355+
))}
356+
</select>
357+
</div>
358+
)}
206359
</div>
207360
);
208361
};
@@ -216,11 +369,19 @@ const dandisetInfo = [];
216369
for (const dandiset of dandisets) {
217370
const person = dandiset.contact_person;
218371
const speciesInDandiset = new Set();
372+
const neurodataTypesInDandiset = new Set();
219373
220374
for (const nwbFile of dandiset.nwbFiles) {
221375
if (nwbFile.subject && nwbFile.subject.species) {
222376
speciesInDandiset.add(nwbFile.subject.species);
223377
}
378+
if (nwbFile.neurodataObjects) {
379+
for (const obj of nwbFile.neurodataObjects) {
380+
if (obj.neurodataType) {
381+
neurodataTypesInDandiset.add(obj.neurodataType);
382+
}
383+
}
384+
}
224385
}
225386
226387
if (!contactPersonToSpecies[person]) {
@@ -240,7 +401,8 @@ for (const dandiset of dandisets) {
240401
dandisetInfo.push({
241402
id: dandiset.dandiset_id,
242403
contactPerson: person,
243-
species: Array.from(speciesInDandiset)
404+
species: Array.from(speciesInDandiset),
405+
neurodataTypes: Array.from(neurodataTypesInDandiset)
244406
});
245407
}
246408
@@ -300,7 +462,8 @@ const useFilteredDandisets = (
300462
// If no filters are set, return empty array
301463
if (
302464
filter.contactPerson === "<not specified>" &&
303-
filter.species === "<not specified>"
465+
filter.species === "<not specified>" &&
466+
filter.neurodataTypes.length === 0
304467
) {
305468
return [];
306469
}
@@ -323,8 +486,18 @@ const useFilteredDandisets = (
323486
return false;
324487
}
325488

489+
// Filter by neurodata types - must have all selected types
490+
if (
491+
filter.neurodataTypes.length > 0 &&
492+
!filter.neurodataTypes.every((type) =>
493+
info.neurodataTypes.includes(type),
494+
)
495+
) {
496+
return false;
497+
}
498+
326499
return true;
327500
})
328501
.map((info) => info.id);
329-
}, [searchData, filter.contactPerson, filter.species]);
502+
}, [searchData, filter.contactPerson, filter.species, filter.neurodataTypes]);
330503
};

0 commit comments

Comments
 (0)