@abi-software/map-utilities 1.7.8-beta.2 → 1.7.8-demo.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.
@@ -0,0 +1,580 @@
1
+ <template>
2
+ <div ref="connectivityList" class="connectivity-list">
3
+ <!-- Error Popover -->
4
+ <el-popover
5
+ width="250"
6
+ :show-arrow="false"
7
+ trigger="manual"
8
+ :teleported="false"
9
+ placement="left-start"
10
+ :visible="connectivityError.hasError && !!connectivityError.errorMessage"
11
+ :popper-class="connectivityError.errorType === 'warning' ? 'connectivity-warning-container' : 'connectivity-error-container'"
12
+ >
13
+ <template #reference>
14
+ <div class="connectivity-alert"
15
+ :style="{ top: alertTop + 'px' }">
16
+ </div>
17
+ </template>
18
+ <template #default>
19
+ <span v-html="connectivityError.errorMessage"></span>
20
+ </template>
21
+ </el-popover>
22
+
23
+ <!-- Origins -->
24
+ <div v-if="origins && originsCombinations.length > 0" class="block">
25
+ <div class="attribute-title-container">
26
+ <span class="attribute-title">Origin</span>
27
+ <el-popover
28
+ width="250"
29
+ trigger="hover"
30
+ :teleported="false"
31
+ popper-class="popover-origin-help"
32
+ >
33
+ <template #reference>
34
+ <el-icon class="info"><el-icon-warning /></el-icon>
35
+ </template>
36
+ <span style="word-break: keep-all">
37
+ <i>Origin</i> {{ originDescription }}
38
+ </span>
39
+ </el-popover>
40
+ </div>
41
+ <div
42
+ v-for="(origin, i) in originsCombinations"
43
+ class="attribute-content"
44
+ :origin-item-label="origin.mapLabel"
45
+ :key="origin.sckanLabel"
46
+ @mouseenter="onConnectivityHovered(origin, $event)"
47
+ @mouseleave="onConnectivityHovered()"
48
+ >
49
+ <el-popover
50
+ width="150"
51
+ trigger="hover"
52
+ :teleported="false"
53
+ popper-class="popover-origin-help"
54
+ >
55
+ <template #reference>
56
+ <el-icon
57
+ class="magnify-glass"
58
+ v-show="origin.mapLabel"
59
+ @click="onConnectivityClicked(origin.mapLabel)"
60
+ >
61
+ <el-icon-search />
62
+ </el-icon>
63
+ </template>
64
+ <span>Search connectivity</span>
65
+ </el-popover>
66
+ <span v-if="origin.sckanLabel.toLowerCase() !== origin.mapLabel.toLowerCase()">
67
+ <s>{{ capitalise(origin.sckanLabel) }}</s>
68
+ <span v-if="origin.mapLabel"> / </span>
69
+ </span>
70
+ <span>{{ capitalise(origin.mapLabel) }}</span>
71
+ </div>
72
+ <el-button
73
+ v-show="
74
+ originsWithDatasets && originsWithDatasets.length > 0 &&
75
+ shouldShowExploreButton(originsWithDatasets)
76
+ "
77
+ class="button"
78
+ id="open-dendrites-button"
79
+ @click="openDendrites"
80
+ >
81
+ Explore origin data
82
+ </el-button>
83
+ </div>
84
+
85
+ <!-- Components -->
86
+ <div
87
+ v-if="components && componentsCombinations.length > 0"
88
+ class="block"
89
+ >
90
+ <div class="attribute-title-container">
91
+ <span class="attribute-title">Components</span>
92
+ </div>
93
+ <div
94
+ v-for="(component, i) in componentsCombinations"
95
+ class="attribute-content"
96
+ :component-item-label="component.mapLabel"
97
+ :key="component.sckanLabel"
98
+ @mouseenter="onConnectivityHovered(component, $event)"
99
+ @mouseleave="onConnectivityHovered()"
100
+ >
101
+ <el-popover
102
+ width="150"
103
+ trigger="hover"
104
+ :teleported="false"
105
+ popper-class="popover-origin-help"
106
+ >
107
+ <template #reference>
108
+ <el-icon
109
+ class="magnify-glass"
110
+ v-show="component.mapLabel"
111
+ @click="onConnectivityClicked(component.mapLabel)"
112
+ >
113
+ <el-icon-search />
114
+ </el-icon>
115
+ </template>
116
+ <span>Search connectivity</span>
117
+ </el-popover>
118
+ <span v-if="component.sckanLabel.toLowerCase() !== component.mapLabel.toLowerCase()">
119
+ <s>{{ capitalise(component.sckanLabel) }}</s>
120
+ <span v-if="component.mapLabel"> / </span>
121
+ </span>
122
+ <span>{{ capitalise(component.mapLabel) }}</span>
123
+ </div>
124
+ </div>
125
+
126
+ <!-- Destinations -->
127
+ <div
128
+ v-if="destinations && destinationsCombinations.length > 0"
129
+ class="block"
130
+ >
131
+ <div class="attribute-title-container">
132
+ <span class="attribute-title">Destination</span>
133
+ <el-popover
134
+ width="250"
135
+ trigger="hover"
136
+ :teleported="false"
137
+ popper-class="popover-origin-help"
138
+ >
139
+ <template #reference>
140
+ <el-icon class="info"><el-icon-warning /></el-icon>
141
+ </template>
142
+ <span style="word-break: keep-all">
143
+ <i>Destination</i> is where the axons terminate
144
+ </span>
145
+ </el-popover>
146
+ </div>
147
+ <div
148
+ v-for="(destination, i) in destinationsCombinations"
149
+ class="attribute-content"
150
+ :destination-item-label="destination.mapLabel"
151
+ :key="destination.sckanLabel"
152
+ @mouseenter="onConnectivityHovered(destination, $event)"
153
+ @mouseleave="onConnectivityHovered()"
154
+ >
155
+ <el-popover
156
+ width="150"
157
+ trigger="hover"
158
+ :teleported="false"
159
+ popper-class="popover-origin-help"
160
+ >
161
+ <template #reference>
162
+ <el-icon
163
+ class="magnify-glass"
164
+ v-show="destination.mapLabel"
165
+ @click="onConnectivityClicked(destination.mapLabel)"
166
+ >
167
+ <el-icon-search />
168
+ </el-icon>
169
+ </template>
170
+ <span>Search connectivity</span>
171
+ </el-popover>
172
+ <span v-if="destination.sckanLabel.toLowerCase() !== destination.mapLabel.toLowerCase()">
173
+ <s>{{ capitalise(destination.sckanLabel) }}</s>
174
+ <span v-if="destination.mapLabel"> / </span>
175
+ </span>
176
+ <span>{{ capitalise(destination.mapLabel) }}</span>
177
+ </div>
178
+ <el-button
179
+ v-show="
180
+ destinationsWithDatasets &&
181
+ destinationsWithDatasets.length > 0 &&
182
+ shouldShowExploreButton(destinationsWithDatasets)
183
+ "
184
+ class="button"
185
+ @click="openAxons"
186
+ >
187
+ Explore destination data
188
+ </el-button>
189
+ </div>
190
+
191
+ <!-- Explore Button -->
192
+ <div
193
+ v-show="
194
+ componentsWithDatasets &&
195
+ componentsWithDatasets.length > 0 &&
196
+ shouldShowExploreButton(componentsWithDatasets)
197
+ "
198
+ class="block"
199
+ >
200
+ <el-button
201
+ class="button"
202
+ @click="openAll"
203
+ >
204
+ Search for data on components
205
+ </el-button>
206
+ </div>
207
+ </div>
208
+ </template>
209
+
210
+ <script>
211
+ import {
212
+ Warning as ElIconWarning,
213
+ Search as ElIconSearch,
214
+ } from '@element-plus/icons-vue'
215
+ import {
216
+ ElButton as Button,
217
+ ElContainer as Container,
218
+ ElIcon as Icon,
219
+ } from 'element-plus'
220
+ import { capitalise } from '../utilities'
221
+
222
+ export default {
223
+ name: 'ConnectivityListNew',
224
+ components: {
225
+ Button,
226
+ Container,
227
+ Icon,
228
+ ElIconWarning,
229
+ ElIconSearch
230
+ },
231
+ props: {
232
+ entry: {
233
+ type: Object,
234
+ default: () => ({
235
+ destinations: [],
236
+ origins: [],
237
+ components: [],
238
+ destinationsWithDatasets: [],
239
+ originsWithDatasets: [],
240
+ componentsWithDatasets: [],
241
+ destinationsCombinations: [],
242
+ originsCombinations: [],
243
+ componentsCombinations: [],
244
+ resource: undefined,
245
+ featuresAlert: undefined,
246
+ }),
247
+ },
248
+ origins: {
249
+ type: Array,
250
+ default: () => []
251
+ },
252
+ components: {
253
+ type: Array,
254
+ default: () => []
255
+ },
256
+ destinations: {
257
+ type: Array,
258
+ default: () => []
259
+ },
260
+ originsWithDatasets: {
261
+ type: Array,
262
+ default: () => []
263
+ },
264
+ componentsWithDatasets: {
265
+ type: Array,
266
+ default: () => []
267
+ },
268
+ destinationsWithDatasets: {
269
+ type: Array,
270
+ default: () => []
271
+ },
272
+ componentsCombinations: {
273
+ type: Array,
274
+ default: () => []
275
+ },
276
+ originsCombinations: {
277
+ type: Array,
278
+ default: () => []
279
+ },
280
+ destinationsCombinations: {
281
+ type: Array,
282
+ default: () => []
283
+ },
284
+ availableAnatomyFacets: {
285
+ type: Array,
286
+ default: () => [],
287
+ },
288
+ connectivityError: {
289
+ type: Object,
290
+ default: () => {},
291
+ }
292
+ },
293
+ data: function () {
294
+ return {
295
+ alertTop: 0,
296
+ originDescriptions: {
297
+ motor: 'is the location of the initial cell body of the circuit',
298
+ sensory: 'is the location of the initial cell body in the PNS circuit',
299
+ },
300
+ facetList: [],
301
+ clearErrorTimeout: null,
302
+ }
303
+ },
304
+ watch: {
305
+ availableAnatomyFacets: {
306
+ handler: function (val) {
307
+ this.convertFacetsToList(val)
308
+ },
309
+ immediate: true,
310
+ deep: true,
311
+ },
312
+ },
313
+ computed: {
314
+ originDescription: function () {
315
+ if (
316
+ this.entry &&
317
+ this.entry.title &&
318
+ this.entry.title.toLowerCase().includes('motor')
319
+ ) {
320
+ return this.originDescriptions.motor
321
+ } else {
322
+ return this.originDescriptions.sensory
323
+ }
324
+ },
325
+ },
326
+ methods: {
327
+ capitalise: function (text) {
328
+ return capitalise(text)
329
+ },
330
+ onConnectivityHovered: function (combination, ele) {
331
+ if (this.clearErrorTimeout) {
332
+ clearTimeout(this.clearErrorTimeout);
333
+ this.clearErrorTimeout = null;
334
+ }
335
+
336
+ // Compute the new error state first
337
+ let newError = { hasError: false, errorType: '', errorMessage: '' };
338
+
339
+ if (combination) {
340
+ if (combination.mapId.length) {
341
+ // If there is mapId, it exists on the map.
342
+ // Show hover highlight on the map.
343
+ const hoveredLabel = combination.mapLabel.toLowerCase();
344
+ this.$emit('connectivity-hovered', hoveredLabel);
345
+
346
+ // If the SCKAN term and the Map term are different, show warning message.
347
+ if (JSON.stringify(combination.sckanId) !== JSON.stringify(combination.mapId)) {
348
+ newError = {
349
+ hasError: true,
350
+ errorType: 'warning',
351
+ errorMessage: `<strong>${combination.sckanLabel}</strong> from the SCKAN
352
+ has been mapped to <strong>${combination.mapLabel}</strong> on the Map.`,
353
+ };
354
+ }
355
+ } else if (combination.sckanId.length) {
356
+ // If there is no mapId but there is sckanId,
357
+ // it means the SCKAN term is not available on the Map.
358
+ newError = {
359
+ hasError: true,
360
+ errorType: 'error',
361
+ errorMessage: `<strong>${combination.sckanLabel}</strong> from the SCKAN
362
+ is not available on the Map.`,
363
+ };
364
+ }
365
+ }
366
+
367
+ if (newError.hasError) {
368
+ // Show new error immediately with content
369
+ this.connectivityError.errorType = newError.errorType;
370
+ this.connectivityError.errorMessage = newError.errorMessage;
371
+ this.connectivityError.hasError = true;
372
+ } else {
373
+ // Hide the popover immediately, then clear content after transition (~300ms)
374
+ // so the popover fades out with content still visible (not as empty box)
375
+ this.connectivityError.hasError = false;
376
+ this.clearErrorTimeout = setTimeout(() => {
377
+ this.connectivityError.errorType = '';
378
+ this.connectivityError.errorMessage = '';
379
+ this.clearErrorTimeout = null;
380
+ }, 350);
381
+ }
382
+
383
+ if (ele) {
384
+ this.alertTop = ele.srcElement.offsetParent.offsetTop + ele.srcElement.offsetTop;
385
+ }
386
+ },
387
+ onConnectivityClicked: function (name) {
388
+ const connectivity = this.connectivityError.errorConnectivities;
389
+ // Remove the invalid term while searching
390
+ const label = connectivity
391
+ ? name.replace(new RegExp(`\\s*,?\\s*${connectivity}\\s*,?\\s*`, 'gi'), '').trim()
392
+ : name;
393
+ this.$emit('connectivity-clicked', label);
394
+ },
395
+ // shouldShowMagnifyGlass: Checks whether the hovered terms contain valid term or not
396
+ shouldShowMagnifyGlass: function (features) {
397
+ const connectivity = this.connectivityError.errorConnectivities;
398
+ return connectivity?.toLowerCase() !== features.toLowerCase();
399
+ },
400
+ // shouldShowExploreButton: Checks if the feature is in the list of available anatomy facets
401
+ shouldShowExploreButton: function (features) {
402
+ // facetList will not be available when there has no Sidebar's data
403
+ if (!this.facetList.length) {
404
+ return true
405
+ }
406
+ for (let i = 0; i < features.length; i++) {
407
+ if (this.facetList.includes(features[i].name.toLowerCase())) {
408
+ return true
409
+ }
410
+ }
411
+ return false
412
+ },
413
+ // convertFacetsToList: Converts the available anatomy facets to a list for easy searching
414
+ convertFacetsToList: function (facets) {
415
+ facets.forEach((facet) => {
416
+ if(facet.children) {
417
+ this.convertFacetsToList(facet.children)
418
+ } else {
419
+ this.facetList.push(facet.label.toLowerCase())
420
+ }
421
+ })
422
+ },
423
+ openAll: function () {
424
+ this.$emit('connectivity-action-click', {
425
+ type: 'Facets',
426
+ labels: this.componentsWithDatasets.map((a) => a.name.toLowerCase()),
427
+ })
428
+ },
429
+ openAxons: function () {
430
+ this.$emit('connectivity-action-click', {
431
+ type: 'Facets',
432
+ labels: this.destinationsWithDatasets.map((a) => a.name.toLowerCase()),
433
+ })
434
+ },
435
+ openDendrites: function () {
436
+ this.$emit('connectivity-action-click', {
437
+ type: 'Facets',
438
+ labels: this.originsWithDatasets.map((a) => a.name.toLowerCase()),
439
+ })
440
+ },
441
+ },
442
+ }
443
+ </script>
444
+
445
+ <style lang="scss" scoped>
446
+ .connectivity-list {
447
+ display: flex;
448
+ flex-direction: column;
449
+ gap: 1rem;
450
+ position: relative;
451
+ }
452
+
453
+ .button {
454
+ margin-left: 0px !important;
455
+ margin-top: 0px !important;
456
+ font-size: 14px !important;
457
+ background-color: $app-primary-color;
458
+ color: #fff;
459
+
460
+ &:hover {
461
+ color: #fff !important;
462
+ background-color: #ac76c5 !important;
463
+ border: 1px solid #ac76c5 !important;
464
+ }
465
+
466
+ & + .button {
467
+ margin-top: 10px !important;
468
+ }
469
+ }
470
+
471
+ .icon {
472
+ right: 0px;
473
+ position: absolute;
474
+ top: 10px;
475
+ }
476
+
477
+ .icon:hover {
478
+ cursor: pointer;
479
+ }
480
+
481
+ :deep(.popover-origin-help.el-popover) {
482
+ text-transform: none !important; // need to overide the tooltip text transform
483
+ border: 1px solid $app-primary-color;
484
+ font-weight: 400;
485
+ font-family: Asap, sans-serif, Helvetica;
486
+
487
+ .el-popper__arrow {
488
+ &:before {
489
+ border-color: $app-primary-color;
490
+ background-color: #ffffff;
491
+ }
492
+ }
493
+ }
494
+
495
+ .info {
496
+ color: #8300bf;
497
+ transform: rotate(180deg);
498
+ margin-left: 8px;
499
+ }
500
+
501
+ .attribute-title-container {
502
+ margin-bottom: 0.5em;
503
+ }
504
+
505
+ .attribute-title {
506
+ font-size: 16px;
507
+ font-weight: 600;
508
+ /* font-weight: bold; */
509
+ text-transform: uppercase;
510
+ }
511
+
512
+ .attribute-content {
513
+ font-size: 14px;
514
+ font-weight: 500;
515
+ transition: color 0.25s ease;
516
+ position: relative;
517
+ cursor: default;
518
+ padding-left: 16px;
519
+
520
+ .magnify-glass {
521
+ display: none;
522
+ position: absolute;
523
+ top: 0;
524
+ left: 0;
525
+ }
526
+
527
+ &:hover {
528
+ color: $app-primary-color;
529
+
530
+ .magnify-glass {
531
+ display: block;
532
+ padding-top: 4px;
533
+ cursor: pointer;
534
+ }
535
+ }
536
+
537
+ + .attribute-content {
538
+ &::before {
539
+ content: "";
540
+ width: 90%;
541
+ height: 1px;
542
+ background-color: var(--el-border-color);
543
+ position: absolute;
544
+ top: 0;
545
+ left: 0;
546
+ }
547
+ }
548
+
549
+ &:last-of-type {
550
+ margin-bottom: 0.5em;
551
+ }
552
+ }
553
+
554
+ .connectivity-alert {
555
+ position: absolute;
556
+ width: 1px;
557
+ right:0px;
558
+ }
559
+
560
+ .connectivity-list :deep(.connectivity-error-container.el-popover),
561
+ .connectivity-list :deep(.connectivity-warning-container.el-popover) {
562
+ min-height: 31px; // placeholder
563
+ align-items: center;
564
+ justify-content: center;
565
+ padding: 0.25rem 0.5rem;
566
+ border-radius: var(--el-border-radius-small);
567
+ pointer-events: none;
568
+ word-break: break-word;
569
+ }
570
+
571
+ .connectivity-list :deep(.connectivity-error-container.el-popover) {
572
+ background-color: var(--el-color-error-light-9);
573
+ border: 1px solid var(--el-color-error);
574
+ }
575
+
576
+ .connectivity-list :deep(.connectivity-warning-container.el-popover) {
577
+ background-color: var(--el-color-warning-light-9);
578
+ border: 1px solid var(--el-color-warning);
579
+ }
580
+ </style>