@abi-software/map-utilities 1.1.3-beta.1 → 1.1.3-beta.11

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.1.3-beta.1",
3
+ "version": "1.1.3-beta.11",
4
4
  "files": [
5
5
  "dist/*",
6
6
  "src/*",
package/src/App.vue CHANGED
@@ -39,6 +39,9 @@ const drawnTypes = [
39
39
  { value: "None", label: "None" },
40
40
  ];
41
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/";
42
45
 
43
46
  onMounted(() => {
44
47
  console.log("🚀 ~ onMounted ~ appRef:", appRef.value);
@@ -513,8 +516,8 @@ function changeHover(value) {
513
516
  />
514
517
  <ConnectivityGraph
515
518
  v-if="showConnectivityGraph"
516
- entry="ilxtr:neuron-type-aacar-13"
517
- map-server="https://mapcore-demo.org/curation/flatmap/"
519
+ :entry="connectivityGraphEntry"
520
+ :map-server="mapServer"
518
521
  />
519
522
  </div>
520
523
  </template>
@@ -533,6 +536,10 @@ function changeHover(value) {
533
536
  top: calc(50% - 100px);
534
537
  left: calc(50% - 200px);
535
538
  }
539
+ .toolbar-container {
540
+ height: 80px;
541
+ position: relative;
542
+ }
536
543
  .connectivity-graph {
537
544
  width: 600px;
538
545
  height: 600px;
@@ -1,15 +1,115 @@
1
1
  <template>
2
- <div class="connectivity-graph">
2
+ <div class="connectivity-graph" v-loading="loading">
3
+
3
4
  <div ref="graphCanvas" class="graph-canvas"></div>
4
- <div class="node-key">
5
- <div class="key-head">Node type:</div>
6
- <div>
7
- <div><span>Node:</span><span class="key-box" style="background: #80F0F0"/></div>
8
- <div><span>Axon:</span><span class="key-box" style="background: green"/></div>
9
- <div><span>Dendrite:</span><span class="key-box" style="background: red"/></div>
10
- <div><span>Both:</span><span class="key-box" style="background: gray"/></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>
11
106
  </div>
12
107
  </div>
108
+
109
+ <div class="connectivity-graph-error" v-if="errorMessage">
110
+ {{ errorMessage }}
111
+ </div>
112
+
13
113
  </div>
14
114
  </template>
15
115
 
@@ -17,6 +117,14 @@
17
117
  import { ConnectivityGraph } from './graph';
18
118
 
19
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';
20
128
 
21
129
  export default {
22
130
  name: 'ConnectivityGraph',
@@ -35,42 +143,121 @@ export default {
35
143
  },
36
144
  data: function () {
37
145
  return {
38
- cy: null,
146
+ loading: true,
39
147
  connectivityGraph: null,
148
+ selectedSource: '',
149
+ pathList: [],
150
+ schemaVersion: '',
40
151
  knowledgeByPath: new Map(),
41
152
  labelledTerms: new Set(),
42
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: '',
43
161
  };
44
162
  },
45
163
  mounted() {
164
+ this.refreshCache();
165
+ this.loadCacheData();
46
166
  this.run().then((res) => {
47
167
  this.showGraph(this.entry);
48
168
  });
49
169
  },
50
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
+ },
51
217
  run: async function () {
52
- const schemaVersion = await this.getSchemaVersion();
53
- if (schemaVersion < MIN_SCHEMA_VERSION) {
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) {
54
224
  console.warn('No Server!');
55
225
  return;
56
226
  }
57
227
  this.showSpinner();
58
- const selectedSource = await this.setSourceList();
59
- await this.setPathList(selectedSource)
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);
60
234
  this.hideSpinner();
61
235
  },
62
236
  showGraph: async function (neuronPath) {
63
237
  const graphCanvas = this.$refs.graphCanvas;
238
+
64
239
  this.showSpinner();
240
+
65
241
  this.connectivityGraph = new ConnectivityGraph(this.labelCache, graphCanvas);
66
242
  await this.connectivityGraph.addConnectivity(this.knowledgeByPath.get(neuronPath));
243
+
67
244
  this.hideSpinner();
245
+
68
246
  this.connectivityGraph.showConnectivity(graphCanvas);
69
- this.currentPath = neuronPath
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
+ });
70
256
  },
71
257
  query: async function (sql, params) {
72
258
  const url = `${this.mapServer}knowledge/query/`;
73
- const query = { sql, params }
259
+ const query = { sql, params };
260
+
74
261
  try {
75
262
  const response = await fetch(url, {
76
263
  method: 'POST',
@@ -81,9 +268,11 @@ export default {
81
268
  },
82
269
  body: JSON.stringify(query)
83
270
  });
271
+
84
272
  if (!response.ok) {
85
273
  throw new Error(`Cannot access ${url}`);
86
274
  }
275
+
87
276
  return await response.json();
88
277
  } catch {
89
278
  return {
@@ -98,38 +287,50 @@ export default {
98
287
  // Order with most recent first...
99
288
  let firstSource = '';
100
289
  const sourceList = [];
290
+
101
291
  for (const source of sources) {
102
292
  if (source) {
103
293
  sourceList.push(source);
294
+
104
295
  if (firstSource === '') {
105
296
  firstSource = source;
106
297
  }
107
298
  }
108
299
  }
300
+
109
301
  return firstSource;
110
302
  },
111
- setPathList: async function (source) {
303
+ loadPathData: async function (source) {
112
304
  const data = await this.query(
113
305
  `select entity, knowledge from knowledge
114
306
  where entity like 'ilxtr:%' and source=?
115
307
  order by entity`,
116
308
  [source]);
117
- const pathList = [];
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
+
118
319
  this.knowledgeByPath.clear();
119
320
  this.labelledTerms = new Set();
120
- for (const [key, jsonKnowledge] of data.values) {
321
+
322
+ for (const [key, jsonKnowledge] of this.pathList) {
121
323
  const knowledge = JSON.parse(jsonKnowledge);
122
324
  if ('connectivity' in knowledge) {
123
- const label = knowledge.label || key;
124
- const shortLabel = (label === key.slice(6).replace('-prime', "'").replaceAll('-', ' '))
125
- ? ''
126
- : (label.length < 50) ? label : `${label.slice(0, 50)}...`;
127
- pathList.push(key);
128
325
  this.knowledgeByPath.set(key, knowledge);
129
326
  this.cacheLabels(knowledge);
130
327
  }
131
328
  }
132
- await this.getCachedTermLabels();
329
+
330
+ if (!this.labelCache.size) {
331
+ await this.getCachedTermLabels();
332
+ }
333
+
133
334
  return '';
134
335
  },
135
336
  getSchemaVersion: async function () {
@@ -146,9 +347,11 @@ export default {
146
347
  "Content-Type": "application/json"
147
348
  }
148
349
  });
350
+
149
351
  if (!response.ok) {
150
352
  console.error(`Cannot access ${url}`);
151
353
  }
354
+
152
355
  return await response.json();
153
356
  } catch {
154
357
  return null;
@@ -156,14 +359,25 @@ export default {
156
359
  },
157
360
  getCachedTermLabels: async function () {
158
361
  if (this.labelledTerms.size) {
159
- const termLabels = await this.query(`
160
- select entity, label from labels
161
- where entity in (?${', ?'.repeat(this.labelledTerms.size-1)})`,
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`,
162
366
  [...this.labelledTerms.values()]
163
367
  );
164
- for (const termLabel of termLabels.values) {
165
- this.labelCache.set(termLabel[0], termLabel[1]);
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
+ }
166
376
  }
377
+
378
+ const labelCacheObj = Object.fromEntries(this.labelCache);
379
+ sessionStorage.setItem('connectivity-graph-labels', JSON.stringify(labelCacheObj));
380
+ this.updateCacheExpiry();
167
381
  }
168
382
  },
169
383
  cacheNodeLabels: function (node) {
@@ -178,10 +392,34 @@ export default {
178
392
  }
179
393
  },
180
394
  showSpinner: function () {
181
- // show loading spinner
395
+ this.loading = true;
182
396
  },
183
397
  hideSpinner: function () {
184
- // hide loading spinner
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);
185
423
  },
186
424
  },
187
425
  };
@@ -201,30 +439,193 @@ export default {
201
439
  border: solid 1px #e4e7ed;
202
440
  }
203
441
 
204
- .node-key {
442
+ .control-panel {
205
443
  position: absolute;
206
- top: 1rem;
207
444
  right: 1rem;
208
- border: 1px solid $app-primary-color;
209
- padding: 4px;
210
- background-color: rgba(240, 240, 240, 0.8);
211
445
 
212
- div div {
213
- width: 90px;
446
+ &-tools {
447
+ top: 1rem;
448
+ }
449
+
450
+ &-nodes {
451
+ bottom: 1rem;
214
452
  }
215
453
  }
216
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
+
217
462
  .key-head {
218
463
  text-align: center;
219
464
  font-weight: bold;
220
- border-bottom: 1px solid gray;
465
+ border-bottom: 1px solid var(--el-border-color);
221
466
  padding-bottom: 4px;
222
- margin-bottom: 4px;
467
+ margin-bottom: 0.5rem;
468
+ }
469
+
470
+ .key-box-container {
471
+ display: flex;
472
+ flex-direction: row;
473
+ gap: 1rem;
223
474
  }
224
475
 
225
476
  .key-box {
226
- float: right;
227
- width: 12px;
228
- height: 12px;
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
+ }
229
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
+ }
230
631
  </style>