@abi-software/map-utilities 1.2.0-beta.8 → 1.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abi-software/map-utilities",
3
- "version": "1.2.0-beta.8",
3
+ "version": "1.2.0",
4
4
  "files": [
5
5
  "dist/*",
6
6
  "src/*",
@@ -31,6 +31,7 @@
31
31
  "dependencies": {
32
32
  "@abi-software/svg-sprite": "^1.0.1",
33
33
  "@element-plus/icons-vue": "^2.3.1",
34
+ "cytoscape": "^3.30.2",
34
35
  "element-plus": "2.8.4",
35
36
  "mitt": "^3.0.1",
36
37
  "vue": "^3.4.21"
package/src/App.vue CHANGED
@@ -38,6 +38,10 @@ const drawnTypes = [
38
38
  { value: "Polygon", label: "Polygon" },
39
39
  { value: "None", label: "None" },
40
40
  ];
41
+ const showConnectivityGraph = ref(false);
42
+ const connectivityGraphEntry = "ilxtr:neuron-type-aacar-13";
43
+ // const connectivityGraphEntry = "ilxtr:sparc-nlp/kidney/134";
44
+ const mapServer = "https://mapcore-demo.org/curation/flatmap/";
41
45
 
42
46
  onMounted(() => {
43
47
  console.log("🚀 ~ onMounted ~ appRef:", appRef.value);
@@ -452,6 +456,25 @@ function confirmCreate(value) {
452
456
  </el-button>
453
457
  </el-col>
454
458
  </el-row>
459
+ <el-row>
460
+ <el-col>
461
+ <h3>Connectivity Graph</h3>
462
+ </el-col>
463
+ <el-col>
464
+ <el-button
465
+ @click="showConnectivityGraph = true"
466
+ size="small"
467
+ >
468
+ Show connectivity graph
469
+ </el-button>
470
+ <el-button
471
+ @click="showConnectivityGraph = false"
472
+ size="small"
473
+ >
474
+ Hide connectivity graph
475
+ </el-button>
476
+ </el-col>
477
+ </el-row>
455
478
 
456
479
  <DrawToolbar
457
480
  v-show="isFlatmap"
@@ -521,7 +544,11 @@ function confirmCreate(value) {
521
544
  @setColour="setColour"
522
545
  @checkChanged="checkChanged"
523
546
  />
524
-
547
+ <ConnectivityGraph
548
+ v-if="showConnectivityGraph"
549
+ :entry="connectivityGraphEntry"
550
+ :map-server="mapServer"
551
+ />
525
552
  </div>
526
553
  </template>
527
554
 
@@ -545,6 +572,14 @@ function confirmCreate(value) {
545
572
  border-style: solid;
546
573
  border-width: 1px;
547
574
  border-color: black;
548
-
575
+ }
576
+ .toolbar-container {
577
+ height: 80px;
578
+ position: relative;
579
+ }
580
+ .connectivity-graph {
581
+ width: 600px;
582
+ height: 600px;
583
+ margin-top: 1rem;
549
584
  }
550
585
  </style>
@@ -0,0 +1,631 @@
1
+ <template>
2
+ <div class="connectivity-graph" v-loading="loading">
3
+
4
+ <div ref="graphCanvas" class="graph-canvas"></div>
5
+
6
+ <div class="control-panel control-panel-tools">
7
+ <div class="tools" :class="{'zoom-locked': zoomEnabled}">
8
+ <el-tooltip
9
+ :content="resetLabel"
10
+ placement="top"
11
+ effect="control-tooltip"
12
+ >
13
+ <el-button
14
+ class="control-button"
15
+ :class="theme"
16
+ size="small"
17
+ @click="reset"
18
+ >
19
+ <el-icon color="white">
20
+ <el-icon-aim />
21
+ </el-icon>
22
+ <span class="visually-hidden">{{ resetLabel }}</span>
23
+ </el-button>
24
+ </el-tooltip>
25
+
26
+ <el-tooltip
27
+ :content="zoomLockLabel"
28
+ placement="top"
29
+ effect="control-tooltip"
30
+ >
31
+ <el-button
32
+ class="control-button"
33
+ :class="theme"
34
+ size="small"
35
+ @click="toggleZoom"
36
+ >
37
+ <el-icon color="white">
38
+ <template v-if="zoomEnabled">
39
+ <el-icon-lock />
40
+ </template>
41
+ <template v-else>
42
+ <el-icon-unlock />
43
+ </template>
44
+ </el-icon>
45
+ <span class="visually-hidden">{{ zoomLockLabel }}</span>
46
+ </el-button>
47
+ </el-tooltip>
48
+
49
+ <el-tooltip
50
+ :content="zoomInLabel"
51
+ placement="left"
52
+ effect="control-tooltip"
53
+ >
54
+ <el-button
55
+ class="control-button"
56
+ :class="theme"
57
+ size="small"
58
+ @click="zoomIn"
59
+ >
60
+ <el-icon color="white">
61
+ <el-icon-zoom-in />
62
+ </el-icon>
63
+ <span class="visually-hidden">{{ zoomInLabel }}</span>
64
+ </el-button>
65
+ </el-tooltip>
66
+
67
+ <el-tooltip
68
+ :content="zoomOutLabel"
69
+ placement="left"
70
+ effect="control-tooltip"
71
+ >
72
+ <el-button
73
+ class="control-button"
74
+ :class="theme"
75
+ size="small"
76
+ @click="zoomOut"
77
+ >
78
+ <el-icon color="white">
79
+ <el-icon-zoom-out />
80
+ </el-icon>
81
+ <span class="visually-hidden">{{ zoomOutLabel }}</span>
82
+ </el-button>
83
+ </el-tooltip>
84
+ </div>
85
+ </div>
86
+
87
+ <div class="control-panel control-panel-nodes">
88
+ <div class="node-key">
89
+ <!-- <div class="key-head">Node type:</div> -->
90
+ <div class="key-box-container">
91
+ <div class="key-box key-box-dendrite">
92
+ Origin
93
+ </div>
94
+ <div class="key-box key-box-node">
95
+ Components
96
+ </div>
97
+ <div class="key-box key-box-axon">
98
+ Destination
99
+ </div>
100
+ <!--
101
+ <div class="key-box key-box-both">
102
+ Both
103
+ </div>
104
+ -->
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <div class="connectivity-graph-error" v-if="errorMessage">
110
+ {{ errorMessage }}
111
+ </div>
112
+
113
+ </div>
114
+ </template>
115
+
116
+ <script>
117
+ import { ConnectivityGraph } from './graph';
118
+
119
+ const MIN_SCHEMA_VERSION = 1.3;
120
+ const CACHE_LIFETIME = 24 * 60 * 60 * 1000; // One day
121
+ const RESET_LABEL = 'Reset position';
122
+ const ZOOM_LOCK_LABEL = 'Lock zoom';
123
+ const ZOOM_UNLOCK_LABEL = 'Unlock zoom';
124
+ const ZOOM_IN_LABEL = 'Zoom in';
125
+ const ZOOM_OUT_LABEL = 'Zoom out';
126
+ const ZOOM_INCREMENT = 0.25;
127
+ const APP_PRIMARY_COLOR = '#8300bf';
128
+
129
+ export default {
130
+ name: 'ConnectivityGraph',
131
+ props: {
132
+ /**
133
+ * Entity to load its connectivity graph.
134
+ */
135
+ entry: {
136
+ type: String,
137
+ default: '',
138
+ },
139
+ mapServer: {
140
+ type: String,
141
+ default: '',
142
+ },
143
+ },
144
+ data: function () {
145
+ return {
146
+ loading: true,
147
+ connectivityGraph: null,
148
+ selectedSource: '',
149
+ pathList: [],
150
+ schemaVersion: '',
151
+ knowledgeByPath: new Map(),
152
+ labelledTerms: new Set(),
153
+ labelCache: new Map(),
154
+ resetLabel: RESET_LABEL,
155
+ zoomLockLabel: ZOOM_LOCK_LABEL,
156
+ zoomInLabel: ZOOM_IN_LABEL,
157
+ zoomOutLabel: ZOOM_OUT_LABEL,
158
+ iconColor: APP_PRIMARY_COLOR,
159
+ zoomEnabled: false,
160
+ errorMessage: '',
161
+ };
162
+ },
163
+ mounted() {
164
+ this.refreshCache();
165
+ this.loadCacheData();
166
+ this.run().then((res) => {
167
+ this.showGraph(this.entry);
168
+ });
169
+ },
170
+ methods: {
171
+ loadCacheData: function () {
172
+ const selectedSource = sessionStorage.getItem('connectivity-graph-source');
173
+ const labelCache = sessionStorage.getItem('connectivity-graph-labels');
174
+ const pathList = sessionStorage.getItem('connectivity-graph-pathlist');
175
+ const schemaVersion = sessionStorage.getItem('connectivity-graph-schema-version');
176
+
177
+ if (selectedSource) {
178
+ this.selectedSource = selectedSource;
179
+ }
180
+ if (pathList) {
181
+ this.pathList = JSON.parse(pathList);
182
+ }
183
+ if (labelCache) {
184
+ const labelCacheObj = JSON.parse(labelCache);
185
+ this.labelCache = new Map(Object.entries(labelCacheObj));
186
+ }
187
+ if (schemaVersion) {
188
+ this.schemaVersion = schemaVersion;
189
+ }
190
+ },
191
+ removeAllCacheData: function () {
192
+ const keys = [
193
+ 'connectivity-graph-expiry',
194
+ 'connectivity-graph-source',
195
+ 'connectivity-graph-labels',
196
+ 'connectivity-graph-pathlist',
197
+ 'connectivity-graph-schema-version',
198
+ ];
199
+ keys.forEach((key) => {
200
+ sessionStorage.removeItem(key);
201
+ });
202
+ },
203
+ refreshCache: function () {
204
+ const expiry = sessionStorage.getItem('connectivity-graph-expiry');
205
+ const now = new Date();
206
+
207
+ if (now.getTime() > expiry) {
208
+ this.removeAllCacheData();
209
+ }
210
+ },
211
+ updateCacheExpiry: function () {
212
+ const now = new Date();
213
+ const expiry = now.getTime() + CACHE_LIFETIME;
214
+
215
+ sessionStorage.setItem('connectivity-graph-expiry', expiry);
216
+ },
217
+ run: async function () {
218
+ if (!this.schemaVersion) {
219
+ this.schemaVersion = await this.getSchemaVersion();
220
+ sessionStorage.setItem('connectivity-graph-schema-version', this.schemaVersion);
221
+ this.updateCacheExpiry();
222
+ }
223
+ if (this.schemaVersion < MIN_SCHEMA_VERSION) {
224
+ console.warn('No Server!');
225
+ return;
226
+ }
227
+ this.showSpinner();
228
+ if (!this.selectedSource) {
229
+ this.selectedSource = await this.setSourceList();
230
+ sessionStorage.setItem('connectivity-graph-source', this.selectedSource);
231
+ this.updateCacheExpiry();
232
+ }
233
+ await this.setPathList(this.selectedSource);
234
+ this.hideSpinner();
235
+ },
236
+ showGraph: async function (neuronPath) {
237
+ const graphCanvas = this.$refs.graphCanvas;
238
+
239
+ this.showSpinner();
240
+
241
+ this.connectivityGraph = new ConnectivityGraph(this.labelCache, graphCanvas);
242
+ await this.connectivityGraph.addConnectivity(this.knowledgeByPath.get(neuronPath));
243
+
244
+ this.hideSpinner();
245
+
246
+ this.connectivityGraph.showConnectivity(graphCanvas);
247
+
248
+ this.connectivityGraph.on('tap-node', (event) => {
249
+ const { label } = event.detail;
250
+ const labels = label ? label.split(`\n`) : [];
251
+ /**
252
+ * This event is triggered after a node on the connectivity graph is clicked.
253
+ */
254
+ this.$emit('tap-node', labels);
255
+ });
256
+ },
257
+ query: async function (sql, params) {
258
+ const url = `${this.mapServer}knowledge/query/`;
259
+ const query = { sql, params };
260
+
261
+ try {
262
+ const response = await fetch(url, {
263
+ method: 'POST',
264
+ headers: {
265
+ "Accept": "application/json; charset=utf-8",
266
+ "Cache-Control": "no-store",
267
+ "Content-Type": "application/json"
268
+ },
269
+ body: JSON.stringify(query)
270
+ });
271
+
272
+ if (!response.ok) {
273
+ throw new Error(`Cannot access ${url}`);
274
+ }
275
+
276
+ return await response.json();
277
+ } catch {
278
+ return {
279
+ values: []
280
+ };
281
+ }
282
+ },
283
+ setSourceList: async function () {
284
+ const data = await this.getJsonData(`${this.mapServer}knowledge/sources`);
285
+ const sources = data ? (data.sources || []) : [];
286
+
287
+ // Order with most recent first...
288
+ let firstSource = '';
289
+ const sourceList = [];
290
+
291
+ for (const source of sources) {
292
+ if (source) {
293
+ sourceList.push(source);
294
+
295
+ if (firstSource === '') {
296
+ firstSource = source;
297
+ }
298
+ }
299
+ }
300
+
301
+ return firstSource;
302
+ },
303
+ loadPathData: async function (source) {
304
+ const data = await this.query(
305
+ `select entity, knowledge from knowledge
306
+ where entity like 'ilxtr:%' and source=?
307
+ order by entity`,
308
+ [source]);
309
+ const pathList = data ? data.values : [];
310
+ return pathList;
311
+ },
312
+ setPathList: async function (source) {
313
+ if (!this.pathList.length) {
314
+ this.pathList = await this.loadPathData(source);
315
+ sessionStorage.setItem('connectivity-graph-pathlist', JSON.stringify(this.pathList));
316
+ this.updateCacheExpiry();
317
+ }
318
+
319
+ this.knowledgeByPath.clear();
320
+ this.labelledTerms = new Set();
321
+
322
+ for (const [key, jsonKnowledge] of this.pathList) {
323
+ const knowledge = JSON.parse(jsonKnowledge);
324
+ if ('connectivity' in knowledge) {
325
+ this.knowledgeByPath.set(key, knowledge);
326
+ this.cacheLabels(knowledge);
327
+ }
328
+ }
329
+
330
+ if (!this.labelCache.size) {
331
+ await this.getCachedTermLabels();
332
+ }
333
+
334
+ return '';
335
+ },
336
+ getSchemaVersion: async function () {
337
+ const data = await this.getJsonData(`${this.mapServer}knowledge/schema-version`);
338
+ return data ? (+data.version || 0) : 0;
339
+ },
340
+ getJsonData: async function (url) {
341
+ try {
342
+ const response = await fetch(url, {
343
+ method: 'GET',
344
+ headers: {
345
+ "Accept": "application/json; charset=utf-8",
346
+ "Cache-Control": "no-store",
347
+ "Content-Type": "application/json"
348
+ }
349
+ });
350
+
351
+ if (!response.ok) {
352
+ console.error(`Cannot access ${url}`);
353
+ }
354
+
355
+ return await response.json();
356
+ } catch {
357
+ return null;
358
+ }
359
+ },
360
+ getCachedTermLabels: async function () {
361
+ if (this.labelledTerms.size) {
362
+ const data = await this.query(
363
+ `select entity, knowledge from knowledge
364
+ where entity in (?${', ?'.repeat(this.labelledTerms.size-1)})
365
+ order by source desc`,
366
+ [...this.labelledTerms.values()]
367
+ );
368
+
369
+ let last_entity = null;
370
+ for (const [key, jsonKnowledge] of data.values) {
371
+ if (key !== last_entity) {
372
+ const knowledge = JSON.parse(jsonKnowledge);
373
+ this.labelCache.set(key, knowledge['label'] || key);
374
+ last_entity = key;
375
+ }
376
+ }
377
+
378
+ const labelCacheObj = Object.fromEntries(this.labelCache);
379
+ sessionStorage.setItem('connectivity-graph-labels', JSON.stringify(labelCacheObj));
380
+ this.updateCacheExpiry();
381
+ }
382
+ },
383
+ cacheNodeLabels: function (node) {
384
+ for (const term of [node[0], ...node[1]]) {
385
+ this.labelledTerms.add(term);
386
+ }
387
+ },
388
+ cacheLabels: async function (knowledge) {
389
+ for (const edge of knowledge.connectivity) {
390
+ this.cacheNodeLabels(edge[0]);
391
+ this.cacheNodeLabels(edge[1]);
392
+ }
393
+ },
394
+ showSpinner: function () {
395
+ this.loading = true;
396
+ },
397
+ hideSpinner: function () {
398
+ this.loading = false;
399
+ },
400
+ reset: function () {
401
+ this.connectivityGraph.reset();
402
+ },
403
+ zoomIn: function () {
404
+ this.connectivityGraph.zoom(ZOOM_INCREMENT);
405
+ },
406
+ zoomOut: function () {
407
+ this.connectivityGraph.zoom(-ZOOM_INCREMENT);
408
+ },
409
+ /**
410
+ * Enable/disable user zoom for scrolling
411
+ */
412
+ toggleZoom: function () {
413
+ this.zoomEnabled = !this.zoomEnabled;
414
+ this.zoomLockLabel = this.zoomEnabled ? ZOOM_UNLOCK_LABEL : ZOOM_LOCK_LABEL;
415
+ this.connectivityGraph.enableZoom(!this.zoomEnabled);
416
+ },
417
+ showErrorMessage: function (errorMessage) {
418
+ this.errorMessage = errorMessage;
419
+ // Show error for 3 seconds
420
+ setTimeout(() => {
421
+ this.errorMessage = '';
422
+ }, 3000);
423
+ },
424
+ },
425
+ };
426
+ </script>
427
+
428
+ <style lang="scss" scoped>
429
+ .connectivity-graph,
430
+ .graph-canvas {
431
+ width: 100%;
432
+ height: 600px;
433
+ background-color: white;
434
+ position: relative;
435
+ }
436
+
437
+ .connectivity-graph {
438
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06);
439
+ border: solid 1px #e4e7ed;
440
+ }
441
+
442
+ .control-panel {
443
+ position: absolute;
444
+ right: 1rem;
445
+
446
+ &-tools {
447
+ top: 1rem;
448
+ }
449
+
450
+ &-nodes {
451
+ bottom: 1rem;
452
+ }
453
+ }
454
+
455
+ .node-key {
456
+ padding: 0.5rem;
457
+ font-size: 12px;
458
+ border: 1px solid var(--el-border-color);
459
+ background-color: rgba(#f7faff, 0.85);
460
+ }
461
+
462
+ .key-head {
463
+ text-align: center;
464
+ font-weight: bold;
465
+ border-bottom: 1px solid var(--el-border-color);
466
+ padding-bottom: 4px;
467
+ margin-bottom: 0.5rem;
468
+ }
469
+
470
+ .key-box-container {
471
+ display: flex;
472
+ flex-direction: row;
473
+ gap: 1rem;
474
+ }
475
+
476
+ .key-box {
477
+ display: flex;
478
+ align-items: center;
479
+ gap: 0.35rem;
480
+ position: relative;
481
+ line-height: 1;
482
+
483
+ &::before {
484
+ content: "";
485
+ display: block;
486
+ width: 14px;
487
+ height: 14px;
488
+ }
489
+
490
+ &-node::before,
491
+ &-both::before {
492
+ border: 1px solid gray;
493
+ border-radius: var(--el-border-radius-small);
494
+ }
495
+
496
+ // &-node {
497
+ // background: #80F0F0;
498
+ // }
499
+
500
+ // &-both {
501
+ // background: gray;
502
+ // }
503
+
504
+ &-axon::before {
505
+ border: 1px solid gray;
506
+ border-radius: var(--el-border-radius-small);
507
+ transform: rotate(45deg);
508
+ // background: green;
509
+ }
510
+
511
+ &-dendrite::before {
512
+ border: 1px solid gray;
513
+ border-radius: 50%;
514
+ // background: red;
515
+ }
516
+ }
517
+
518
+ .tools {
519
+ display: grid;
520
+ grid-template-columns: repeat(2, 1fr);
521
+ grid-template-rows: repeat(3, 1fr);
522
+ gap: 0.5rem;
523
+
524
+ :deep(.el-button:nth-child(3)) {
525
+ grid-column: 2;
526
+ grid-row: 2;
527
+ }
528
+
529
+ :deep(.el-button:nth-child(4)) {
530
+ grid-column: 2;
531
+ grid-row: 3;
532
+ }
533
+
534
+ :deep(.el-button:nth-child(3)),
535
+ :deep(.el-button:nth-child(4)) {
536
+ opacity: 0;
537
+ visibility: hidden;
538
+ pointer-events: none;
539
+ transform: translateY(-100%);
540
+ transition: all 0.25s ease;
541
+ }
542
+
543
+ &.zoom-locked {
544
+ :deep(.el-button:nth-child(3)),
545
+ :deep(.el-button:nth-child(4)) {
546
+ opacity: 1;
547
+ visibility: visible;
548
+ pointer-events: initial;
549
+ transform: translateY(0%);
550
+ }
551
+
552
+ :deep(.el-button:nth-child(4)) {
553
+ transition-delay: 0.125s;
554
+ }
555
+ }
556
+ }
557
+
558
+ .control-button {
559
+ width: 24px;
560
+ height: 24px;
561
+ margin: 0 !important;
562
+ padding: 0 !important;
563
+ font-size: 16px !important;
564
+ border-color: $app-primary-color !important;
565
+ border-radius: 50%;
566
+ background: $app-primary-color !important;
567
+ transition: all 0.25s ease;
568
+
569
+ svg {
570
+ margin: 0;
571
+ }
572
+
573
+ &,
574
+ &:focus,
575
+ &:active {
576
+ box-shadow: none !important;
577
+ }
578
+ }
579
+
580
+ :deep(.cy-graph-tooltip) {
581
+ padding: 4px 10px;
582
+ font-family: Asap;
583
+ font-size: 12px;
584
+ background: #f3ecf6 !important;
585
+ border: 1px solid $app-primary-color;
586
+ border-radius: var(--el-border-radius-base);
587
+ position: relative;
588
+ top: 0;
589
+ left: 0;
590
+ width: fit-content;
591
+ z-index: 1;
592
+ }
593
+
594
+ .connectivity-graph-error {
595
+ position: absolute;
596
+ top: 1rem;
597
+ left: 50%;
598
+ transform: translateX(-50%);
599
+ width: fit-content;
600
+ font-size: 12px;
601
+ padding: 0.25rem 0.5rem;
602
+ background-color: var(--el-color-error-light-9);
603
+ border-radius: var(--el-border-radius-small);
604
+ border: 1px solid var(--el-color-error);
605
+ }
606
+
607
+ .visually-hidden {
608
+ clip: rect(0 0 0 0);
609
+ clip-path: inset(50%);
610
+ height: 1px;
611
+ overflow: hidden;
612
+ position: absolute;
613
+ white-space: nowrap;
614
+ width: 1px;
615
+ }
616
+ </style>
617
+
618
+ <style lang="scss">
619
+ .el-popper.is-control-tooltip {
620
+ padding: 4px 10px;
621
+ font-family: Asap;
622
+ background: #f3ecf6 !important;
623
+ border: 1px solid $app-primary-color;
624
+
625
+ & .el-popper__arrow::before {
626
+ border: 1px solid;
627
+ border-color: $app-primary-color;
628
+ background: #f3ecf6;
629
+ }
630
+ }
631
+ </style>