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

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