1
1
import { FunctionComponent , useEffect , useState , useMemo } from "react" ;
2
+ import { Chip , Stack } from "@mui/material" ;
2
3
import { JobRunnerClient } from "./jobRunnerClient" ;
3
4
4
5
type ExperimentalSearchPanelProps = {
@@ -9,6 +10,7 @@ type DandisetInfo = {
9
10
id : string ;
10
11
contactPerson : string ;
11
12
species : string [ ] ;
13
+ neurodataTypes : string [ ] ;
12
14
} ;
13
15
14
16
type ContactPersonWithSpecies = {
@@ -25,6 +27,7 @@ type SearchData = {
25
27
type Filter = {
26
28
contactPerson : string | "<not specified>" ;
27
29
species : string | "<not specified>" ;
30
+ neurodataTypes : string [ ] ;
28
31
} ;
29
32
30
33
export const ExperimentalSearchPanel : FunctionComponent <
@@ -55,55 +58,138 @@ export const ExperimentalSearchPanel: FunctionComponent<
55
58
const [ filter , setFilter ] = useState < Filter > ( {
56
59
contactPerson : "<not specified>" ,
57
60
species : "<not specified>" ,
61
+ neurodataTypes : [ ] ,
58
62
} ) ;
59
63
const { searchData } = useSearchData ( jobRunnerClient ) ;
60
64
const dandisetIds = useFilteredDandisets ( searchData , filter ) ;
61
65
62
66
const availableContactPersons = useMemo ( ( ) => {
63
67
if ( ! searchData ) return [ ] ;
64
- if ( filter . species === "<not specified>" ) {
65
- return searchData . contactPersons ;
66
- }
67
68
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
+ ) ,
74
153
} ) ) ;
75
- } , [ searchData , filter . species ] ) ;
154
+ } , [ searchData , filter . contactPerson , filter . species , filter . neurodataTypes ] ) ;
76
155
77
156
const availableSpecies = useMemo ( ( ) => {
78
157
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
- }
95
158
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
+ } ) ;
101
177
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 ] ) ;
107
193
108
194
// Clear selections if they become unavailable
109
195
useEffect ( ( ) => {
@@ -129,11 +215,32 @@ export const ExperimentalSearchPanel: FunctionComponent<
129
215
setFilter ( ( f ) => ( { ...f , contactPerson : "<not specified>" } ) ) ;
130
216
}
131
217
}
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
+ }
132
237
} , [
133
238
filter . species ,
134
239
filter . contactPerson ,
240
+ filter . neurodataTypes ,
135
241
availableSpecies ,
136
242
availableContactPersons ,
243
+ availableNeurodataTypes ,
137
244
] ) ;
138
245
139
246
useEffect ( ( ) => {
@@ -203,6 +310,52 @@ export const ExperimentalSearchPanel: FunctionComponent<
203
310
</ select >
204
311
</ div >
205
312
) }
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
+ ) }
206
359
</ div >
207
360
) ;
208
361
} ;
@@ -216,11 +369,19 @@ const dandisetInfo = [];
216
369
for (const dandiset of dandisets) {
217
370
const person = dandiset.contact_person;
218
371
const speciesInDandiset = new Set();
372
+ const neurodataTypesInDandiset = new Set();
219
373
220
374
for (const nwbFile of dandiset.nwbFiles) {
221
375
if (nwbFile.subject && nwbFile.subject.species) {
222
376
speciesInDandiset.add(nwbFile.subject.species);
223
377
}
378
+ if (nwbFile.neurodataObjects) {
379
+ for (const obj of nwbFile.neurodataObjects) {
380
+ if (obj.neurodataType) {
381
+ neurodataTypesInDandiset.add(obj.neurodataType);
382
+ }
383
+ }
384
+ }
224
385
}
225
386
226
387
if (!contactPersonToSpecies[person]) {
@@ -240,7 +401,8 @@ for (const dandiset of dandisets) {
240
401
dandisetInfo.push({
241
402
id: dandiset.dandiset_id,
242
403
contactPerson: person,
243
- species: Array.from(speciesInDandiset)
404
+ species: Array.from(speciesInDandiset),
405
+ neurodataTypes: Array.from(neurodataTypesInDandiset)
244
406
});
245
407
}
246
408
@@ -300,7 +462,8 @@ const useFilteredDandisets = (
300
462
// If no filters are set, return empty array
301
463
if (
302
464
filter . contactPerson === "<not specified>" &&
303
- filter . species === "<not specified>"
465
+ filter . species === "<not specified>" &&
466
+ filter . neurodataTypes . length === 0
304
467
) {
305
468
return [ ] ;
306
469
}
@@ -323,8 +486,18 @@ const useFilteredDandisets = (
323
486
return false ;
324
487
}
325
488
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
+
326
499
return true ;
327
500
} )
328
501
. map ( ( info ) => info . id ) ;
329
- } , [ searchData , filter . contactPerson , filter . species ] ) ;
502
+ } , [ searchData , filter . contactPerson , filter . species , filter . neurodataTypes ] ) ;
330
503
} ;
0 commit comments