@abi-software/map-side-bar 2.19.1-demo.0 → 2.20.0-acuppoints.0

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