@abi-software/map-utilities 1.1.3-beta.0 → 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.0",
3
+ "version": "1.1.3-beta.10",
4
4
  "files": [
5
5
  "dist/*",
6
6
  "src/*",
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);
@@ -422,6 +426,25 @@ function changeHover(value) {
422
426
  </el-button>
423
427
  </el-col>
424
428
  </el-row>
429
+ <el-row>
430
+ <el-col>
431
+ <h3>Connectivity Graph</h3>
432
+ </el-col>
433
+ <el-col>
434
+ <el-button
435
+ @click="showConnectivityGraph = true"
436
+ size="small"
437
+ >
438
+ Show connectivity graph
439
+ </el-button>
440
+ <el-button
441
+ @click="showConnectivityGraph = false"
442
+ size="small"
443
+ >
444
+ Hide connectivity graph
445
+ </el-button>
446
+ </el-col>
447
+ </el-row>
425
448
 
426
449
  <DrawToolbar
427
450
  v-show="isFlatmap"
@@ -491,6 +514,11 @@ function changeHover(value) {
491
514
  @setColour="setColour"
492
515
  @checkChanged="checkChanged"
493
516
  />
517
+ <ConnectivityGraph
518
+ v-if="showConnectivityGraph"
519
+ :entry="connectivityGraphEntry"
520
+ :map-server="mapServer"
521
+ />
494
522
  </div>
495
523
  </template>
496
524
 
@@ -508,4 +536,13 @@ function changeHover(value) {
508
536
  top: calc(50% - 100px);
509
537
  left: calc(50% - 200px);
510
538
  }
539
+ .toolbar-container {
540
+ height: 80px;
541
+ position: relative;
542
+ }
543
+ .connectivity-graph {
544
+ width: 600px;
545
+ height: 600px;
546
+ margin-top: 1rem;
547
+ }
511
548
  </style>
@@ -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,39 +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
  }
109
- // this.#sourceSelector.innerHTML = sourceList.join('')
298
+
110
299
  return firstSource;
111
300
  },
112
- setPathList: async function (source) {
301
+ loadPathData: async function (source) {
113
302
  const data = await this.query(
114
303
  `select entity, knowledge from knowledge
115
304
  where entity like 'ilxtr:%' and source=?
116
305
  order by entity`,
117
306
  [source]);
118
- 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
+
119
317
  this.knowledgeByPath.clear();
120
318
  this.labelledTerms = new Set();
121
- for (const [key, jsonKnowledge] of data.values) {
319
+
320
+ for (const [key, jsonKnowledge] of this.pathList) {
122
321
  const knowledge = JSON.parse(jsonKnowledge);
123
322
  if ('connectivity' in knowledge) {
124
- const label = knowledge.label || key;
125
- const shortLabel = (label === key.slice(6).replace('-prime', "'").replaceAll('-', ' '))
126
- ? ''
127
- : (label.length < 50) ? label : `${label.slice(0, 50)}...`;
128
- pathList.push(key);
129
323
  this.knowledgeByPath.set(key, knowledge);
130
324
  this.cacheLabels(knowledge);
131
325
  }
132
326
  }
133
- await this.getCachedTermLabels();
327
+
328
+ if (!this.labelCache.size) {
329
+ await this.getCachedTermLabels();
330
+ }
331
+
134
332
  return '';
135
333
  },
136
334
  getSchemaVersion: async function () {
@@ -147,9 +345,11 @@ export default {
147
345
  "Content-Type": "application/json"
148
346
  }
149
347
  });
348
+
150
349
  if (!response.ok) {
151
350
  console.error(`Cannot access ${url}`);
152
351
  }
352
+
153
353
  return await response.json();
154
354
  } catch {
155
355
  return null;
@@ -157,14 +357,25 @@ export default {
157
357
  },
158
358
  getCachedTermLabels: async function () {
159
359
  if (this.labelledTerms.size) {
160
- const termLabels = await this.query(`
161
- select entity, label from labels
162
- 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`,
163
364
  [...this.labelledTerms.values()]
164
365
  );
165
- for (const termLabel of termLabels.values) {
166
- 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
+ }
167
374
  }
375
+
376
+ const labelCacheObj = Object.fromEntries(this.labelCache);
377
+ sessionStorage.setItem('connectivity-graph-labels', JSON.stringify(labelCacheObj));
378
+ this.updateCacheExpiry();
168
379
  }
169
380
  },
170
381
  cacheNodeLabels: function (node) {
@@ -179,10 +390,34 @@ export default {
179
390
  }
180
391
  },
181
392
  showSpinner: function () {
182
- // show loading spinner
393
+ this.loading = true;
183
394
  },
184
395
  hideSpinner: function () {
185
- // 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);
186
421
  },
187
422
  },
188
423
  };
@@ -202,30 +437,192 @@ export default {
202
437
  border: solid 1px #e4e7ed;
203
438
  }
204
439
 
205
- .node-key {
440
+ .control-panel {
206
441
  position: absolute;
207
- top: 1rem;
208
442
  right: 1rem;
209
- border: 1px solid $app-primary-color;
210
- padding: 4px;
211
- background-color: rgba(240, 240, 240, 0.8);
212
443
 
213
- div div {
214
- width: 90px;
444
+ &-tools {
445
+ top: 1rem;
446
+ }
447
+
448
+ &-nodes {
449
+ bottom: 1rem;
215
450
  }
216
451
  }
217
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
+
218
460
  .key-head {
219
461
  text-align: center;
220
462
  font-weight: bold;
221
- border-bottom: 1px solid gray;
463
+ border-bottom: 1px solid var(--el-border-color);
222
464
  padding-bottom: 4px;
223
- margin-bottom: 4px;
465
+ margin-bottom: 0.5rem;
466
+ }
467
+
468
+ .key-box-container {
469
+ display: flex;
470
+ flex-direction: row;
471
+ gap: 1rem;
224
472
  }
225
473
 
226
474
  .key-box {
227
- float: right;
228
- width: 12px;
229
- 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
+ }
230
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
+ }
231
628
  </style>