@abi-software/map-side-bar 2.14.6-demo.0 → 2.14.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/map-side-bar.js +8739 -9797
- package/dist/map-side-bar.umd.cjs +72 -77
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/App.vue +4 -11
- package/src/assets/connectivity-explorer.scss +3 -7
- package/src/components/ConnectivityCard.vue +2 -26
- package/src/components/ConnectivityInfo.vue +46 -3
- package/src/components/SearchFilters.vue +9 -10
- package/src/components/SearchHistory.vue +14 -1
- package/src/components/SideBar.vue +1 -82
- package/src/components/Tabs.vue +6 -15
- package/src/components.d.ts +0 -3
- package/src/components/CellCard.vue +0 -946
- package/src/components/CellCardExplorer.vue +0 -754
- package/src/components/icons/IconOpenExternal.vue +0 -28
- package/src/utils/common.js +0 -71
|
@@ -1,754 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<el-card :body-style="bodyStyle" class="content-card">
|
|
3
|
-
<template #header>
|
|
4
|
-
<div class="header">
|
|
5
|
-
<div class="search-input-container" :class="{'is-focus': searchInput}">
|
|
6
|
-
<el-input
|
|
7
|
-
class="search-input"
|
|
8
|
-
placeholder="Search"
|
|
9
|
-
v-model="searchInput"
|
|
10
|
-
@keyup="searchEvent"
|
|
11
|
-
clearable
|
|
12
|
-
@clear="clearSearchClicked"
|
|
13
|
-
></el-input>
|
|
14
|
-
<el-popover
|
|
15
|
-
width="350"
|
|
16
|
-
trigger="hover"
|
|
17
|
-
popper-class="filter-help-popover"
|
|
18
|
-
>
|
|
19
|
-
<template #reference>
|
|
20
|
-
<MapSvgIcon icon="help" class="help" />
|
|
21
|
-
</template>
|
|
22
|
-
<div>
|
|
23
|
-
<strong>Search rules:</strong>
|
|
24
|
-
<ul>
|
|
25
|
-
<li>
|
|
26
|
-
<strong>Multiple Terms:</strong> Separate terms with a comma (<code>,</code>).
|
|
27
|
-
This will find cell cards that match any of the terms (an "OR" search).
|
|
28
|
-
</li>
|
|
29
|
-
<li>
|
|
30
|
-
<strong>Exact Phrase:</strong> Terms within a comma block will be matched as an exact phrase.
|
|
31
|
-
</li>
|
|
32
|
-
</ul>
|
|
33
|
-
<br/>
|
|
34
|
-
<strong>Examples:</strong>
|
|
35
|
-
<ul>
|
|
36
|
-
<li>
|
|
37
|
-
<strong>To find by exact phrase:</strong>
|
|
38
|
-
Searching for <code>soma location</code> will find any card containing <code>"soma location"</code>.
|
|
39
|
-
</li>
|
|
40
|
-
<li>
|
|
41
|
-
<strong>To find by multiple terms:</strong>
|
|
42
|
-
Searching for <code>mouse, vagal</code> will find cards that contain either <code>mouse</code> OR <code>vagal</code>.
|
|
43
|
-
</li>
|
|
44
|
-
</ul>
|
|
45
|
-
</div>
|
|
46
|
-
</el-popover>
|
|
47
|
-
</div>
|
|
48
|
-
<el-button
|
|
49
|
-
type="primary"
|
|
50
|
-
class="button"
|
|
51
|
-
@click="searchEvent"
|
|
52
|
-
size="large"
|
|
53
|
-
>
|
|
54
|
-
Search
|
|
55
|
-
</el-button>
|
|
56
|
-
<el-button
|
|
57
|
-
link
|
|
58
|
-
class="el-button-link"
|
|
59
|
-
@click="onResetClick"
|
|
60
|
-
size="large"
|
|
61
|
-
>
|
|
62
|
-
Reset
|
|
63
|
-
</el-button>
|
|
64
|
-
</div>
|
|
65
|
-
</template>
|
|
66
|
-
|
|
67
|
-
<SearchFilters
|
|
68
|
-
class="filters"
|
|
69
|
-
ref="filtersRef"
|
|
70
|
-
:entry="filterEntry"
|
|
71
|
-
:envVars="envVars"
|
|
72
|
-
@filterResults="filterUpdate"
|
|
73
|
-
@numberPerPage="numberPerPageUpdate"
|
|
74
|
-
@loading="filtersLoading"
|
|
75
|
-
@cascaderReady="cascaderReady"
|
|
76
|
-
></SearchFilters>
|
|
77
|
-
|
|
78
|
-
<SearchHistory
|
|
79
|
-
ref="searchHistory"
|
|
80
|
-
localStorageKey="sparc.science-cell-card-search-history"
|
|
81
|
-
@search="searchHistorySearch"
|
|
82
|
-
></SearchHistory>
|
|
83
|
-
|
|
84
|
-
<div class="content scrollbar" v-loading="loadingCards" ref="content">
|
|
85
|
-
<CellCard
|
|
86
|
-
v-for="cellType in cellTypes"
|
|
87
|
-
:key="cellType.id"
|
|
88
|
-
:cellType="cellType"
|
|
89
|
-
:isActive="activeCardId === cellType.id"
|
|
90
|
-
@open="openCard(cellType.id)"
|
|
91
|
-
@close="closeCard"
|
|
92
|
-
@dataset-search="onDatasetSearch"
|
|
93
|
-
@connectivity-search="onConnectivitySearch"
|
|
94
|
-
@soma-location-hovered="showSomaLocation"
|
|
95
|
-
/>
|
|
96
|
-
<el-pagination
|
|
97
|
-
class="pagination"
|
|
98
|
-
v-model:current-page="page"
|
|
99
|
-
hide-on-single-page
|
|
100
|
-
large
|
|
101
|
-
layout="prev, pager, next"
|
|
102
|
-
:page-size="numberPerPage"
|
|
103
|
-
:total="totalFilteredCount"
|
|
104
|
-
@current-change="pageChange"
|
|
105
|
-
></el-pagination>
|
|
106
|
-
</div>
|
|
107
|
-
</el-card>
|
|
108
|
-
</template>
|
|
109
|
-
|
|
110
|
-
<script>
|
|
111
|
-
/* eslint-disable no-alert, no-console */
|
|
112
|
-
import {
|
|
113
|
-
ElButton as Button,
|
|
114
|
-
ElCard as Card,
|
|
115
|
-
ElInput as Input,
|
|
116
|
-
ElPagination as Pagination,
|
|
117
|
-
} from 'element-plus'
|
|
118
|
-
import 'element-plus/es/components/message/style/css';
|
|
119
|
-
import SearchFilters from './SearchFilters.vue'
|
|
120
|
-
import SearchHistory from './SearchHistory.vue'
|
|
121
|
-
import EventBus from './EventBus.js'
|
|
122
|
-
import CellCard from './CellCard.vue'
|
|
123
|
-
import { capitalise, generateUUID } from '../utils/common.js';
|
|
124
|
-
import { MapSvgIcon } from '@abi-software/svg-sprite';
|
|
125
|
-
|
|
126
|
-
let cachedCellCardsData = null
|
|
127
|
-
let pendingCellCardsRequest = null
|
|
128
|
-
|
|
129
|
-
export default {
|
|
130
|
-
components: {
|
|
131
|
-
SearchFilters,
|
|
132
|
-
SearchHistory,
|
|
133
|
-
Button,
|
|
134
|
-
Card,
|
|
135
|
-
CellCard,
|
|
136
|
-
Input,
|
|
137
|
-
MapSvgIcon,
|
|
138
|
-
Pagination,
|
|
139
|
-
},
|
|
140
|
-
name: 'CellCardExplorer',
|
|
141
|
-
emits: ['soma-location-hovered', 'dataset-search', 'connectivity-search', 'soma-locations-ready'],
|
|
142
|
-
props: {
|
|
143
|
-
envVars: {
|
|
144
|
-
type: Object,
|
|
145
|
-
default: () => {},
|
|
146
|
-
},
|
|
147
|
-
activeSpecies: {
|
|
148
|
-
type: Array,
|
|
149
|
-
default: () => [],
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
data: function () {
|
|
153
|
-
return {
|
|
154
|
-
bodyStyle: {
|
|
155
|
-
flex: '1 1 auto',
|
|
156
|
-
'flex-flow': 'column',
|
|
157
|
-
display: 'flex',
|
|
158
|
-
},
|
|
159
|
-
allCellTypes: [],
|
|
160
|
-
cellTypes: [],
|
|
161
|
-
filterOptions: [],
|
|
162
|
-
activeFilters: [],
|
|
163
|
-
searchInput: '',
|
|
164
|
-
numberPerPage: 10,
|
|
165
|
-
page: 1,
|
|
166
|
-
start: 0,
|
|
167
|
-
totalFilteredCount: 0,
|
|
168
|
-
loadingCards: false,
|
|
169
|
-
activeCardId: null,
|
|
170
|
-
cascaderIsReady: false,
|
|
171
|
-
geneBaseToDisplay: {},
|
|
172
|
-
geneDisplayToBase: {},
|
|
173
|
-
};
|
|
174
|
-
},
|
|
175
|
-
computed: {
|
|
176
|
-
filterEntry: function () {
|
|
177
|
-
return {
|
|
178
|
-
numberOfHits: this.totalFilteredCount,
|
|
179
|
-
filterFacets: this.activeFilters,
|
|
180
|
-
showFilters: true,
|
|
181
|
-
options: this.filterOptions,
|
|
182
|
-
helper: {
|
|
183
|
-
within: "'mouse' OR 'human'",
|
|
184
|
-
between: "'species' AND 'soma location'",
|
|
185
|
-
},
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
watch: {
|
|
190
|
-
activeSpecies: {
|
|
191
|
-
deep: true,
|
|
192
|
-
immediate: true,
|
|
193
|
-
handler: 'syncActiveSpeciesFilters',
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
mounted: function() {
|
|
197
|
-
this.fetchCellTypes(this.envVars.CELL_CARDS_API);
|
|
198
|
-
},
|
|
199
|
-
methods: {
|
|
200
|
-
syncCascaderFromActiveFilters: function() {
|
|
201
|
-
if (!this.cascaderIsReady || !this.$refs.filtersRef) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (this.activeFilters.length) {
|
|
206
|
-
this.$refs.filtersRef.setCascader(this.activeFilters);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
this.$refs.filtersRef.checkShowAllBoxes();
|
|
211
|
-
},
|
|
212
|
-
syncActiveSpeciesFilters: function() {
|
|
213
|
-
this.page = 1;
|
|
214
|
-
this.start = 0;
|
|
215
|
-
const normalizedActiveSpecies = this.getValidatedActiveSpecies();
|
|
216
|
-
const nonSpeciesFilters = this.activeFilters.filter((activeFilter) => {
|
|
217
|
-
return activeFilter?.term?.toLowerCase() !== 'species';
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const speciesFilters = normalizedActiveSpecies.map((species) => {
|
|
221
|
-
return {
|
|
222
|
-
facetPropPath: 'species',
|
|
223
|
-
facet: capitalise(species),
|
|
224
|
-
term: 'Species',
|
|
225
|
-
tagLabel: capitalise(species),
|
|
226
|
-
};
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
this.activeFilters = [...nonSpeciesFilters, ...speciesFilters];
|
|
230
|
-
this.syncCascaderFromActiveFilters();
|
|
231
|
-
this.applyFilters(this.activeFilters);
|
|
232
|
-
this.emitSomaLocations(this.filterOptions);
|
|
233
|
-
},
|
|
234
|
-
getCellCardsData: async function(url) {
|
|
235
|
-
if (cachedCellCardsData) {
|
|
236
|
-
return cachedCellCardsData;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (!pendingCellCardsRequest) {
|
|
240
|
-
pendingCellCardsRequest = fetch(url)
|
|
241
|
-
.then((response) => {
|
|
242
|
-
if (!response.ok) {
|
|
243
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
244
|
-
}
|
|
245
|
-
return response.json();
|
|
246
|
-
})
|
|
247
|
-
.then((data) => {
|
|
248
|
-
cachedCellCardsData = data;
|
|
249
|
-
return data;
|
|
250
|
-
})
|
|
251
|
-
.finally(() => {
|
|
252
|
-
pendingCellCardsRequest = null;
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return pendingCellCardsRequest;
|
|
257
|
-
},
|
|
258
|
-
fetchCellTypes: async function(url) {
|
|
259
|
-
if (url) {
|
|
260
|
-
this.loadingCards = true;
|
|
261
|
-
try {
|
|
262
|
-
const data = await this.getCellCardsData(url);
|
|
263
|
-
if (data.DEFAULT_CELL_TYPES) {
|
|
264
|
-
const loadedCellTypes = data.DEFAULT_CELL_TYPES.map((cellType) => {
|
|
265
|
-
return {
|
|
266
|
-
...cellType,
|
|
267
|
-
id: generateUUID(),
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
this.setGeneMappings(data.DEFAULT_GENES);
|
|
272
|
-
this.allCellTypes = loadedCellTypes;
|
|
273
|
-
this.filterOptions = this.buildFilterOptions(loadedCellTypes);
|
|
274
|
-
this.emitSomaLocations(this.filterOptions);
|
|
275
|
-
this.syncActiveSpeciesFilters();
|
|
276
|
-
}
|
|
277
|
-
} catch (error) {
|
|
278
|
-
console.error('Error fetching cell types:', error);
|
|
279
|
-
} finally {
|
|
280
|
-
this.loadingCards = false;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
},
|
|
284
|
-
activateCard: function(cardId) {
|
|
285
|
-
this.activeCardId = this.activeCardId === cardId ? null : cardId;
|
|
286
|
-
},
|
|
287
|
-
openCard: function(cardId) {
|
|
288
|
-
this.activeCardId = cardId;
|
|
289
|
-
},
|
|
290
|
-
closeCard: function() {
|
|
291
|
-
this.activeCardId = null;
|
|
292
|
-
},
|
|
293
|
-
openSearch: function(filters, query) {
|
|
294
|
-
this.page = 1;
|
|
295
|
-
this.start = 0;
|
|
296
|
-
this.searchInput = String(query || '').trim();
|
|
297
|
-
|
|
298
|
-
const openSearchFilters = filters.map((filter) => {
|
|
299
|
-
return {
|
|
300
|
-
facetPropPath: 'somaLocations',
|
|
301
|
-
facet: capitalise(filter.facet || ''),
|
|
302
|
-
term: filter.term || '',
|
|
303
|
-
tagLabel: capitalise(filter.facet || ''),
|
|
304
|
-
};
|
|
305
|
-
});
|
|
306
|
-
const normalizedActiveSpecies = this.getValidatedActiveSpecies();
|
|
307
|
-
const speciesFilters = normalizedActiveSpecies.map((species) => {
|
|
308
|
-
return {
|
|
309
|
-
facetPropPath: 'species',
|
|
310
|
-
facet: capitalise(species),
|
|
311
|
-
term: 'Species',
|
|
312
|
-
tagLabel: capitalise(species),
|
|
313
|
-
};
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
this.activeFilters = [...openSearchFilters, ...speciesFilters];
|
|
317
|
-
this.syncCascaderFromActiveFilters();
|
|
318
|
-
this.applyFilters(this.activeFilters);
|
|
319
|
-
this.emitSomaLocations(this.filterOptions);
|
|
320
|
-
this.searchHistoryUpdate(this.activeFilters, this.searchInput);
|
|
321
|
-
this.$nextTick(() => this.scrollToTop());
|
|
322
|
-
},
|
|
323
|
-
clearSearchClicked: function() {
|
|
324
|
-
this.searchInput = '';
|
|
325
|
-
this.searchAndFilterUpdate();
|
|
326
|
-
},
|
|
327
|
-
searchEvent: function(event = false) {
|
|
328
|
-
if (event.keyCode === 13 || event instanceof MouseEvent) {
|
|
329
|
-
this.searchInput = this.searchInput.trim();
|
|
330
|
-
this.searchAndFilterUpdate();
|
|
331
|
-
}
|
|
332
|
-
},
|
|
333
|
-
onResetClick: function() {
|
|
334
|
-
this.searchInput = '';
|
|
335
|
-
this.activeFilters = [];
|
|
336
|
-
this.page = 1;
|
|
337
|
-
this.start = 0;
|
|
338
|
-
this.$refs.filtersRef?.checkShowAllBoxes();
|
|
339
|
-
if (this.$refs.searchHistory) {
|
|
340
|
-
this.$refs.searchHistory.selectValue = 'Search history';
|
|
341
|
-
}
|
|
342
|
-
this.applyFilters(this.activeFilters);
|
|
343
|
-
},
|
|
344
|
-
searchAndFilterUpdate: function() {
|
|
345
|
-
this.page = 1;
|
|
346
|
-
this.start = 0;
|
|
347
|
-
this.applyFilters(this.activeFilters);
|
|
348
|
-
this.searchHistoryUpdate(this.activeFilters, this.searchInput);
|
|
349
|
-
},
|
|
350
|
-
filterUpdate: function(filters) {
|
|
351
|
-
this.activeFilters = [...filters];
|
|
352
|
-
this.page = 1;
|
|
353
|
-
this.start = 0;
|
|
354
|
-
this.applyFilters(this.activeFilters);
|
|
355
|
-
this.searchHistoryUpdate(this.activeFilters, this.searchInput);
|
|
356
|
-
this.loadingCards = false;
|
|
357
|
-
},
|
|
358
|
-
numberPerPageUpdate: function(value) {
|
|
359
|
-
this.numberPerPage = parseInt(value, 10) || 10;
|
|
360
|
-
this.pageChange(1);
|
|
361
|
-
},
|
|
362
|
-
pageChange: function(page) {
|
|
363
|
-
this.page = page;
|
|
364
|
-
this.start = (page - 1) * this.numberPerPage;
|
|
365
|
-
this.applyFilters(this.activeFilters);
|
|
366
|
-
this.scrollToTop();
|
|
367
|
-
},
|
|
368
|
-
scrollToTop: function() {
|
|
369
|
-
if (this.$refs.content) {
|
|
370
|
-
this.$refs.content.scroll({ top: 0, behavior: 'smooth' });
|
|
371
|
-
}
|
|
372
|
-
},
|
|
373
|
-
filtersLoading: function() {
|
|
374
|
-
// SearchFilters only emits loading:true and never emits false.
|
|
375
|
-
// CellCardExplorer filters synchronously, so loading is reset in filterUpdate.
|
|
376
|
-
},
|
|
377
|
-
searchHistoryUpdate: function(filters, search) {
|
|
378
|
-
if (this.$refs.searchHistory) {
|
|
379
|
-
this.$refs.searchHistory.selectValue = 'Search history';
|
|
380
|
-
// save history only if there has value
|
|
381
|
-
if (filters.length || search?.trim()) {
|
|
382
|
-
this.$refs.searchHistory.addSearchToHistory(filters, search);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
},
|
|
386
|
-
searchHistorySearch: function(item) {
|
|
387
|
-
this.searchInput = item.search || '';
|
|
388
|
-
this.activeFilters = Array.isArray(item.filters) ? [...item.filters] : [];
|
|
389
|
-
this.page = 1;
|
|
390
|
-
this.start = 0;
|
|
391
|
-
this.applyFilters(this.activeFilters);
|
|
392
|
-
|
|
393
|
-
if (this.$refs.filtersRef) {
|
|
394
|
-
if (this.activeFilters.length) {
|
|
395
|
-
this.$refs.filtersRef.setCascader(this.activeFilters);
|
|
396
|
-
} else {
|
|
397
|
-
this.$refs.filtersRef.checkShowAllBoxes();
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
},
|
|
401
|
-
cascaderReady: function() {
|
|
402
|
-
this.cascaderIsReady = true;
|
|
403
|
-
this.syncCascaderFromActiveFilters();
|
|
404
|
-
},
|
|
405
|
-
normalizeFacetValue: function(value) {
|
|
406
|
-
return String(value || '').trim().toLowerCase();
|
|
407
|
-
},
|
|
408
|
-
getAvailableSpeciesSet: function() {
|
|
409
|
-
const availableSpecies = new Set();
|
|
410
|
-
|
|
411
|
-
this.allCellTypes.forEach((cellType) => {
|
|
412
|
-
const species = this.normalizeFacetValue(cellType?.species);
|
|
413
|
-
if (species) {
|
|
414
|
-
availableSpecies.add(species);
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
return availableSpecies;
|
|
419
|
-
},
|
|
420
|
-
getValidatedActiveSpecies: function() {
|
|
421
|
-
const availableSpecies = this.getAvailableSpeciesSet();
|
|
422
|
-
const normalizedActiveSpecies = this.activeSpecies
|
|
423
|
-
.map((species) => this.normalizeActiveSpeciesFilterTerm(species))
|
|
424
|
-
.filter(Boolean);
|
|
425
|
-
|
|
426
|
-
return [...new Set(normalizedActiveSpecies)].filter((species) => {
|
|
427
|
-
return availableSpecies.has(species);
|
|
428
|
-
});
|
|
429
|
-
},
|
|
430
|
-
getCellTypesForActiveSpecies: function() {
|
|
431
|
-
const normalizedActiveSpecies = this.getValidatedActiveSpecies();
|
|
432
|
-
if (!normalizedActiveSpecies.length) {
|
|
433
|
-
return this.allCellTypes;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const activeSpeciesSet = new Set(normalizedActiveSpecies);
|
|
437
|
-
return this.allCellTypes.filter((cellType) => {
|
|
438
|
-
const normalizedCellTypeSpecies = this.normalizeActiveSpeciesFilterTerm(cellType?.species);
|
|
439
|
-
return activeSpeciesSet.has(normalizedCellTypeSpecies);
|
|
440
|
-
});
|
|
441
|
-
},
|
|
442
|
-
// To update the species from the flatmap,
|
|
443
|
-
// mainly from "human male" and "human female" to "human".
|
|
444
|
-
normalizeActiveSpeciesFilterTerm: function(value) {
|
|
445
|
-
const normalized = this.normalizeFacetValue(value);
|
|
446
|
-
if (normalized === 'human male' || normalized === 'human female') {
|
|
447
|
-
return 'human';
|
|
448
|
-
}
|
|
449
|
-
return normalized;
|
|
450
|
-
},
|
|
451
|
-
setGeneMappings: function(genes) {
|
|
452
|
-
const baseToDisplay = {};
|
|
453
|
-
const displayToBase = {};
|
|
454
|
-
|
|
455
|
-
(Array.isArray(genes) ? genes : []).forEach((gene) => {
|
|
456
|
-
const base = this.normalizeFacetValue(gene?.base);
|
|
457
|
-
const display = String(gene?.display || '').trim();
|
|
458
|
-
const normalizedDisplay = this.normalizeFacetValue(display);
|
|
459
|
-
|
|
460
|
-
if (!base) return;
|
|
461
|
-
|
|
462
|
-
if (display) {
|
|
463
|
-
baseToDisplay[base] = display;
|
|
464
|
-
displayToBase[normalizedDisplay] = base;
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
baseToDisplay[base] = base;
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
this.geneBaseToDisplay = baseToDisplay;
|
|
472
|
-
this.geneDisplayToBase = displayToBase;
|
|
473
|
-
},
|
|
474
|
-
normalizeSearchTerms: function(query) {
|
|
475
|
-
return String(query || '')
|
|
476
|
-
.split(',')
|
|
477
|
-
.map((term) => term.trim().toLowerCase())
|
|
478
|
-
.filter(Boolean);
|
|
479
|
-
},
|
|
480
|
-
getCellTypeSearchText: function(cellType) {
|
|
481
|
-
const relatedCellLabels = (cellType.relatedCells || [])
|
|
482
|
-
.map((relatedCell) => relatedCell.label)
|
|
483
|
-
.filter(Boolean)
|
|
484
|
-
.join(' ');
|
|
485
|
-
|
|
486
|
-
return [
|
|
487
|
-
cellType.preferredLabel,
|
|
488
|
-
cellType.entity,
|
|
489
|
-
cellType.species,
|
|
490
|
-
(cellType.somaLocations || []).join(' '),
|
|
491
|
-
cellType.circuitRole,
|
|
492
|
-
cellType.creLine,
|
|
493
|
-
cellType.geneExpressionString,
|
|
494
|
-
cellType.fiberTypeString,
|
|
495
|
-
cellType.physiologyString,
|
|
496
|
-
cellType.sourceNomenclatureLabel,
|
|
497
|
-
relatedCellLabels,
|
|
498
|
-
]
|
|
499
|
-
.filter(Boolean)
|
|
500
|
-
.join(' ')
|
|
501
|
-
.toLowerCase();
|
|
502
|
-
},
|
|
503
|
-
matchSearchQuery: function(cellType, searchTerms) {
|
|
504
|
-
if (!searchTerms.length) {
|
|
505
|
-
return true;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const searchableText = this.getCellTypeSearchText(cellType);
|
|
509
|
-
return searchTerms.some((term) => searchableText.includes(term));
|
|
510
|
-
},
|
|
511
|
-
buildFacetChildren: function(cellTypes, key) {
|
|
512
|
-
const values = new Set();
|
|
513
|
-
|
|
514
|
-
cellTypes.forEach((cellType) => {
|
|
515
|
-
const fieldValue = cellType[key];
|
|
516
|
-
if (Array.isArray(fieldValue)) {
|
|
517
|
-
fieldValue.forEach((item) => {
|
|
518
|
-
const normalized = String(item || '').trim();
|
|
519
|
-
if (normalized) values.add(normalized);
|
|
520
|
-
});
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const normalized = String(fieldValue || '').trim();
|
|
525
|
-
if (normalized) values.add(normalized);
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
return [...values]
|
|
529
|
-
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
|
|
530
|
-
.map((value) => ({ label: value }));
|
|
531
|
-
},
|
|
532
|
-
buildFilterOptions: function(cellTypes) {
|
|
533
|
-
const options = [
|
|
534
|
-
{
|
|
535
|
-
key: 'species',
|
|
536
|
-
label: 'Species',
|
|
537
|
-
children: this.buildFacetChildren(cellTypes, 'species'),
|
|
538
|
-
},
|
|
539
|
-
{
|
|
540
|
-
key: 'somaLocations',
|
|
541
|
-
label: 'Soma location',
|
|
542
|
-
children: this.buildFacetChildren(cellTypes, 'somaLocations'),
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
key: 'geneBaseNames',
|
|
546
|
-
label: 'Gene',
|
|
547
|
-
children: this.buildGeneFacetChildren(cellTypes),
|
|
548
|
-
},
|
|
549
|
-
{
|
|
550
|
-
key: 'sourceNomenclatureLabel',
|
|
551
|
-
label: 'Source',
|
|
552
|
-
children: this.buildFacetChildren(cellTypes, 'sourceNomenclatureLabel'),
|
|
553
|
-
},
|
|
554
|
-
];
|
|
555
|
-
|
|
556
|
-
return options.filter((option) => option.children.length > 0);
|
|
557
|
-
},
|
|
558
|
-
emitSomaLocations: function(filterOptions) {
|
|
559
|
-
const somaLocationOption = (filterOptions || []).find((option) => option.key === 'somaLocations');
|
|
560
|
-
const availableDataRaw = localStorage.getItem('available-name-curie-mapping');
|
|
561
|
-
const availableData = availableDataRaw ? JSON.parse(availableDataRaw) : {};
|
|
562
|
-
const scopedCellTypes = this.getCellTypesForActiveSpecies();
|
|
563
|
-
const somaLocationCounts = scopedCellTypes.reduce((counts, cellType) => {
|
|
564
|
-
(Array.isArray(cellType?.somaLocations) ? cellType.somaLocations : []).forEach((location) => {
|
|
565
|
-
const normalizedLocation = String(location || '').trim().toLowerCase();
|
|
566
|
-
if (!normalizedLocation) return;
|
|
567
|
-
counts.set(normalizedLocation, (counts.get(normalizedLocation) || 0) + 1);
|
|
568
|
-
});
|
|
569
|
-
return counts;
|
|
570
|
-
}, new Map());
|
|
571
|
-
const somaLocations = (somaLocationOption?.children || [])
|
|
572
|
-
.map((child) => String(child?.label || '').trim())
|
|
573
|
-
.filter(Boolean)
|
|
574
|
-
.map((label) => {
|
|
575
|
-
const curie = Object.keys(availableData).find(
|
|
576
|
-
(curie) => String(availableData[curie] || '').toLowerCase() === label.toLowerCase()
|
|
577
|
-
) || '';
|
|
578
|
-
|
|
579
|
-
return {
|
|
580
|
-
label,
|
|
581
|
-
curie,
|
|
582
|
-
count: somaLocationCounts.get(label.toLowerCase()) || 0,
|
|
583
|
-
};
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
const uniqueSomaLocations = [...new Map(
|
|
587
|
-
somaLocations.map((item) => [item.label.toLowerCase(), item])
|
|
588
|
-
).values()];
|
|
589
|
-
|
|
590
|
-
this.$emit('soma-locations-ready', uniqueSomaLocations);
|
|
591
|
-
},
|
|
592
|
-
buildGeneFacetChildren: function(cellTypes) {
|
|
593
|
-
const values = new Set();
|
|
594
|
-
|
|
595
|
-
cellTypes.forEach((cellType) => {
|
|
596
|
-
(cellType.geneBaseNames || []).forEach((geneBaseName) => {
|
|
597
|
-
const normalizedBase = this.normalizeFacetValue(geneBaseName);
|
|
598
|
-
if (normalizedBase) values.add(normalizedBase);
|
|
599
|
-
});
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
return [...values]
|
|
603
|
-
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
|
|
604
|
-
.map((base) => ({
|
|
605
|
-
label: this.geneBaseToDisplay[base] || base,
|
|
606
|
-
}));
|
|
607
|
-
},
|
|
608
|
-
matchFieldFilter: function(cellType, filter) {
|
|
609
|
-
const filterFacet = this.normalizeFacetValue(filter.facet);
|
|
610
|
-
const filterTerm = this.normalizeFacetValue(filter.term);
|
|
611
|
-
|
|
612
|
-
if (!filterFacet || filterFacet === 'show all') {
|
|
613
|
-
return true;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
if (filterTerm === 'species') {
|
|
617
|
-
return this.normalizeFacetValue(cellType.species) === filterFacet;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
if (filterTerm === 'soma location') {
|
|
621
|
-
return (cellType.somaLocations || []).some((location) => {
|
|
622
|
-
return this.normalizeFacetValue(location) === filterFacet;
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
if (filterTerm === 'gene') {
|
|
627
|
-
const selectedGeneBase = this.geneDisplayToBase[filterFacet] || filterFacet;
|
|
628
|
-
return (cellType.geneBaseNames || []).some((geneBaseName) => {
|
|
629
|
-
return this.normalizeFacetValue(geneBaseName) === selectedGeneBase;
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
if (filterTerm === 'circuit role') {
|
|
634
|
-
return this.normalizeFacetValue(cellType.circuitRole) === filterFacet;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
if (filterTerm === 'source') {
|
|
638
|
-
return this.normalizeFacetValue(cellType.sourceNomenclatureLabel) === filterFacet;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
return false;
|
|
642
|
-
},
|
|
643
|
-
applyFilters: function(filters) {
|
|
644
|
-
const searchTerms = this.normalizeSearchTerms(this.searchInput);
|
|
645
|
-
const activeFilters = (filters || []).filter((filter) => {
|
|
646
|
-
return filter?.term && filter?.facet && this.normalizeFacetValue(filter.facet) !== 'show all';
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
const filtersByTerm = activeFilters.reduce((grouped, filter) => {
|
|
650
|
-
const termKey = this.normalizeFacetValue(filter.term);
|
|
651
|
-
if (!grouped[termKey]) {
|
|
652
|
-
grouped[termKey] = [];
|
|
653
|
-
}
|
|
654
|
-
grouped[termKey].push(filter);
|
|
655
|
-
return grouped;
|
|
656
|
-
}, {});
|
|
657
|
-
|
|
658
|
-
const filterGroups = Object.values(filtersByTerm);
|
|
659
|
-
|
|
660
|
-
const filtered = this.allCellTypes.filter((cellType) => {
|
|
661
|
-
return filterGroups.every((termGroup) => {
|
|
662
|
-
return termGroup.some((filter) => this.matchFieldFilter(cellType, filter));
|
|
663
|
-
});
|
|
664
|
-
}).filter((cellType) => this.matchSearchQuery(cellType, searchTerms));
|
|
665
|
-
|
|
666
|
-
this.totalFilteredCount = filtered.length;
|
|
667
|
-
this.cellTypes = filtered.slice(this.start, this.start + this.numberPerPage);
|
|
668
|
-
|
|
669
|
-
if (!this.cellTypes.some((cellType) => cellType.id === this.activeCardId)) {
|
|
670
|
-
this.activeCardId = null;
|
|
671
|
-
}
|
|
672
|
-
},
|
|
673
|
-
showSomaLocation: function (name) {
|
|
674
|
-
this.$emit('soma-location-hovered', name);
|
|
675
|
-
},
|
|
676
|
-
onDatasetSearch: function (query) {
|
|
677
|
-
this.$emit('dataset-search', query);
|
|
678
|
-
},
|
|
679
|
-
onConnectivitySearch: function (query) {
|
|
680
|
-
this.$emit('connectivity-search', query);
|
|
681
|
-
},
|
|
682
|
-
},
|
|
683
|
-
}
|
|
684
|
-
</script>
|
|
685
|
-
|
|
686
|
-
<style lang="scss" scoped>
|
|
687
|
-
@import '../assets/searchPopover.scss';
|
|
688
|
-
@import '../assets/pagination.scss';
|
|
689
|
-
|
|
690
|
-
.content-card {
|
|
691
|
-
height: 100%;
|
|
692
|
-
flex-flow: column;
|
|
693
|
-
display: flex;
|
|
694
|
-
border: 0;
|
|
695
|
-
border-top-right-radius: 0;
|
|
696
|
-
border-bottom-right-radius: 0;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
.content-card :deep(.el-card__header) {
|
|
700
|
-
background-color: #292b66;
|
|
701
|
-
padding: 1rem;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
.content-card :deep(.el-card__body) {
|
|
705
|
-
background-color: #f7faff;
|
|
706
|
-
overflow-y: hidden;
|
|
707
|
-
padding: 1rem;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
.content {
|
|
711
|
-
// width: 515px;
|
|
712
|
-
flex: 1 1 auto;
|
|
713
|
-
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06);
|
|
714
|
-
border: solid 1px #e4e7ed;
|
|
715
|
-
background-color: #ffffff;
|
|
716
|
-
overflow-y: scroll;
|
|
717
|
-
scrollbar-width: thin;
|
|
718
|
-
border-radius: var(--el-border-radius-base);
|
|
719
|
-
position: relative;
|
|
720
|
-
max-height: 100%;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
.scrollbar::-webkit-scrollbar-track {
|
|
724
|
-
border-radius: 10px;
|
|
725
|
-
background-color: #f5f5f5;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
.scrollbar::-webkit-scrollbar {
|
|
729
|
-
width: 12px;
|
|
730
|
-
right: -12px;
|
|
731
|
-
background-color: #f5f5f5;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
.scrollbar::-webkit-scrollbar-thumb {
|
|
735
|
-
border-radius: 4px;
|
|
736
|
-
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.06);
|
|
737
|
-
background-color: #979797;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
.el-button-link {
|
|
741
|
-
color: white !important;
|
|
742
|
-
text-decoration: underline;
|
|
743
|
-
text-underline-offset: 2px;
|
|
744
|
-
border-color: transparent !important;
|
|
745
|
-
background-color: transparent !important;
|
|
746
|
-
padding: 2px !important;
|
|
747
|
-
height: auto !important;
|
|
748
|
-
|
|
749
|
-
&:hover {
|
|
750
|
-
text-decoration-color: transparent;
|
|
751
|
-
box-shadow: none !important;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
</style>
|