@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.
@@ -0,0 +1,355 @@
1
+ /*==============================================================================
2
+
3
+ A viewer for neuron connectivity graphs.
4
+
5
+ Copyright (c) 2019 - 2024 David Brooks
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
18
+
19
+ ==============================================================================*/
20
+
21
+ import cytoscape from 'cytoscape'
22
+
23
+ //==============================================================================
24
+
25
+
26
+ //==============================================================================
27
+
28
+ export class ConnectivityGraph extends EventTarget
29
+ {
30
+ cyg = null
31
+ nodes = []
32
+ edges = []
33
+ axons = []
34
+ dendrites = []
35
+ somas = []
36
+ labelCache = new Map()
37
+ graphCanvas = null
38
+
39
+ constructor(labelCache, graphCanvas)
40
+ {
41
+ super()
42
+ this.labelCache = labelCache;
43
+ this.graphCanvas = graphCanvas;
44
+ }
45
+
46
+ async addConnectivity(knowledge)
47
+ //=====================================================
48
+ {
49
+ this.axons = knowledge.axons.map(node => JSON.stringify(node))
50
+ this.dendrites = knowledge.dendrites.map(node => JSON.stringify(node))
51
+ if (knowledge.somas?.length) {
52
+ this.somas = knowledge.somas.map(node => JSON.stringify(node))
53
+ }
54
+ if (knowledge.connectivity.length) {
55
+ for (const edge of knowledge.connectivity) {
56
+ const e0 = await this.graphNode(edge[0])
57
+ const e1 = await this.graphNode(edge[1])
58
+ this.nodes.push(e0)
59
+ this.nodes.push(e1)
60
+ this.edges.push({
61
+ id: `${e0.id}_${e1.id}`,
62
+ source: e0.id,
63
+ target: e1.id
64
+ })
65
+ }
66
+ } else {
67
+ this.nodes.push({
68
+ id: 'MISSING',
69
+ label: 'NO PATHS'
70
+ })
71
+ }
72
+ }
73
+
74
+ showConnectivity(graphCanvas)
75
+ //================
76
+ {
77
+ this.cyg = new CytoscapeGraph(this, graphCanvas)
78
+
79
+ this.cyg.on('tap-node', (event) => {
80
+ const tapEvent = new CustomEvent('tap-node', {
81
+ detail: event.detail
82
+ })
83
+ this.dispatchEvent(tapEvent);
84
+ });
85
+ }
86
+
87
+ clearConnectivity()
88
+ //=================
89
+ {
90
+ if (this.cyg?.cy) {
91
+ this.cyg.cy.remove()
92
+ this.cyg.cy = null
93
+ }
94
+ }
95
+
96
+ reset()
97
+ //=====
98
+ {
99
+ if (this.cyg?.cy) {
100
+ this.cyg.cy.reset()
101
+ }
102
+ }
103
+
104
+ zoom(val)
105
+ //=======
106
+ {
107
+ if (this.cyg?.cy) {
108
+ const currentZoom = this.cyg.cy.zoom()
109
+ const width = this.cyg.cy.width()
110
+ const height = this.cyg.cy.height()
111
+ const positionToRender = {
112
+ x: width/2,
113
+ y: height/2,
114
+ }
115
+ this.cyg.cy.zoom({
116
+ level: currentZoom + val,
117
+ renderedPosition: positionToRender,
118
+ })
119
+ }
120
+ }
121
+
122
+ enableZoom(option)
123
+ //================
124
+ {
125
+ if (this.cyg?.cy) {
126
+ this.cyg.cy.userZoomingEnabled(option)
127
+ }
128
+ }
129
+
130
+ get elements()
131
+ //============
132
+ {
133
+ return [
134
+ ...this.nodes.map(n => { return {data: n}}),
135
+ ...this.edges.map(e => { return {data: e}})
136
+ ]
137
+ }
138
+
139
+ get roots()
140
+ //===================
141
+ {
142
+ return [
143
+ ...this.dendrites,
144
+ ...this.somas
145
+ ]
146
+ }
147
+
148
+ async graphNode(node)
149
+ //=======================================================
150
+ {
151
+ const id = JSON.stringify(node)
152
+ const label = [node[0], ...node[1]]
153
+ const humanLabels = []
154
+ for (const term of label) {
155
+ const humanLabel = this.labelCache.has(term) ? this.labelCache.get(term) : ''
156
+ humanLabels.push(humanLabel)
157
+ }
158
+ label.push(...humanLabels)
159
+
160
+ const result = {
161
+ id,
162
+ label: label.join('\n')
163
+ }
164
+ if (this.axons.includes(id)) {
165
+ if (this.dendrites.includes(id) || this.somas.includes(id)) {
166
+ result['both-a-d'] = true
167
+ } else {
168
+ result['axon'] = true
169
+ }
170
+ } else if (this.dendrites.includes(id) || this.somas.includes(id)) {
171
+ result['dendrite'] = true
172
+
173
+ }
174
+ return result
175
+ }
176
+
177
+ on(eventName, callback)
178
+ //=====================
179
+ {
180
+ this.addEventListener(eventName, callback)
181
+ }
182
+ }
183
+
184
+ //==============================================================================
185
+
186
+ const GRAPH_STYLE = [
187
+ {
188
+ 'selector': 'node',
189
+ 'style': {
190
+ 'label': function(ele) { return trimLabel(ele.data('label')) },
191
+ // 'background-color': '#80F0F0',
192
+ 'background-color': 'transparent',
193
+ 'background-opacity': '0',
194
+ 'text-valign': 'center',
195
+ 'text-wrap': 'wrap',
196
+ 'width': '80px',
197
+ 'height': '80px',
198
+ 'text-max-width': '80px',
199
+ 'font-size': '6px',
200
+ 'shape': 'round-rectangle',
201
+ 'border-width': 1,
202
+ 'border-style': 'solid',
203
+ 'border-color': 'gray',
204
+ }
205
+ },
206
+ {
207
+ 'selector': 'node[axon]',
208
+ 'style': {
209
+ // 'background-color': 'green',
210
+ 'shape': 'round-diamond',
211
+ 'width': '100px',
212
+ 'height': '100px',
213
+ }
214
+ },
215
+ {
216
+ 'selector': 'node[dendrite]',
217
+ 'style': {
218
+ // 'background-color': 'red',
219
+ 'shape': 'ellipse',
220
+ }
221
+ },
222
+ {
223
+ 'selector': 'node[both-a-d]',
224
+ 'style': {
225
+ // 'background-color': 'gray',
226
+ 'shape': 'round-rectangle',
227
+ }
228
+ },
229
+ {
230
+ 'selector': 'edge',
231
+ 'style': {
232
+ 'width': 1,
233
+ 'line-color': 'dimgray',
234
+ 'target-arrow-color': 'dimgray',
235
+ 'target-arrow-shape': 'triangle',
236
+ 'curve-style': 'bezier'
237
+ }
238
+ }
239
+ ]
240
+
241
+ function trimLabel(label) {
242
+ const labels = label.split('\n')
243
+ const half = labels.length/2
244
+ const trimLabels = labels.slice(half)
245
+ return trimLabels.join('\n')
246
+ }
247
+
248
+ function capitalizeLabels(input) {
249
+ return input.split('\n').map(label => {
250
+ if (label && label[0] >= 'a' && label[0] <= 'z') {
251
+ return label.charAt(0).toUpperCase() + label.slice(1)
252
+ }
253
+ return label
254
+ }).join('\n')
255
+ }
256
+
257
+ //==============================================================================
258
+
259
+ class CytoscapeGraph extends EventTarget
260
+ {
261
+ cy
262
+ tooltip
263
+
264
+ constructor(connectivityGraph, graphCanvas)
265
+ {
266
+ super()
267
+ this.cy = cytoscape({
268
+ container: graphCanvas,
269
+ elements: connectivityGraph.elements,
270
+ layout: {
271
+ name: 'breadthfirst',
272
+ circle: false,
273
+ roots: connectivityGraph.roots
274
+ },
275
+ directed: true,
276
+ style: GRAPH_STYLE,
277
+ minZoom: 0.5,
278
+ maxZoom: 10,
279
+ wheelSensitivity: 0.4,
280
+ }).on('mouseover', 'node', this.overNode.bind(this))
281
+ .on('mouseout', 'node', this.exitNode.bind(this))
282
+ .on('position', 'node', this.moveNode.bind(this))
283
+ .on('tap', this.tapNode.bind(this))
284
+
285
+ this.tooltip = document.createElement('div')
286
+ this.tooltip.className = 'cy-graph-tooltip'
287
+ this.tooltip.hidden = true
288
+ graphCanvas?.lastChild?.appendChild(this.tooltip)
289
+ }
290
+
291
+ remove()
292
+ //======
293
+ {
294
+ if (this.cy) {
295
+ this.cy.destroy()
296
+ }
297
+ }
298
+
299
+ checkRightBoundary(leftPos)
300
+ //==================================
301
+ {
302
+ if ((leftPos + this.tooltip.offsetWidth) >= this.tooltip.parentElement?.offsetWidth) {
303
+ this.tooltip.style.left = `${leftPos - this.tooltip.offsetWidth}px`
304
+ }
305
+ }
306
+
307
+ overNode(event)
308
+ //==============
309
+ {
310
+ const node = event.target
311
+ const label = capitalizeLabels(node.data().label)
312
+
313
+ this.tooltip.innerText = label
314
+ this.tooltip.style.left = `${event.renderedPosition.x}px`
315
+ this.tooltip.style.top = `${event.renderedPosition.y}px`
316
+ this.tooltip.style.maxWidth = '240px'
317
+ this.tooltip.hidden = false
318
+
319
+ this.checkRightBoundary(event.renderedPosition.x)
320
+ }
321
+
322
+ moveNode(event)
323
+ //==============
324
+ {
325
+ const node = event.target
326
+ this.tooltip.style.left = `${node.renderedPosition().x}px`
327
+ this.tooltip.style.top = `${node.renderedPosition().y}px`
328
+ this.checkRightBoundary(node.renderedPosition().x)
329
+ }
330
+
331
+ exitNode(event)
332
+ //==============
333
+ {
334
+ this.tooltip.hidden = true
335
+ }
336
+
337
+ tapNode(event)
338
+ //============
339
+ {
340
+ const node = event.target
341
+ const data = node.data()
342
+ const tapEvent = new CustomEvent('tap-node', {
343
+ detail: data
344
+ })
345
+ this.dispatchEvent(tapEvent);
346
+ }
347
+
348
+ on(eventName, callback)
349
+ //=====================
350
+ {
351
+ this.addEventListener(eventName, callback)
352
+ }
353
+ }
354
+
355
+ //==============================================================================
@@ -202,7 +202,7 @@ export default {
202
202
  methods: {
203
203
  filterNode: function(value, data) {
204
204
  if (!value) return true;
205
- return data.label ? data.label.toLowerCase().includes(value) : false;
205
+ return data.label ? data.label.toLowerCase().includes(value.toLowerCase()) : false;
206
206
  },
207
207
  setColour: function (nodeData, value) {
208
208
  this.$emit("setColour", nodeData, value);
@@ -368,7 +368,7 @@ export default {
368
368
 
369
369
  :deep(.el-checkbox__label) {
370
370
  padding-left: 5px;
371
- color: $app-primary-color !important;
371
+ color: inherit !important;
372
372
  font-size: 12px;
373
373
  font-weight: 500;
374
374
  letter-spacing: 0px;
@@ -385,7 +385,7 @@ export default {
385
385
 
386
386
  .region-tree-node {
387
387
  flex: 1;
388
- color: $app-primary-color !important;
388
+ color: inherit !important;
389
389
  display: flex;
390
390
  font-size: 12px;
391
391
  line-height: 14px;
@@ -1,9 +1,19 @@
1
1
  import AnnotationPopup from "./Tooltip/AnnotationPopup.vue";
2
2
  import CreateTooltipContent from "./Tooltip/CreateTooltipContent.vue";
3
+ import ConnectivityGraph from "./ConnectivityGraph/ConnectivityGraph.vue";
3
4
  import CopyToClipboard from "./CopyToClipboard/CopyToClipboard.vue";
4
5
  import DrawToolbar from "./DrawToolbar/DrawToolbar.vue";
5
6
  import HelpModeDialog from "./HelpModeDialog/HelpModeDialog.vue";
6
7
  import Tooltip from "./Tooltip/Tooltip.vue";
7
8
  import TreeControls from "./TreeControls/TreeControls.vue";
8
9
 
9
- export { AnnotationPopup, CreateTooltipContent, CopyToClipboard, DrawToolbar, HelpModeDialog, Tooltip, TreeControls };
10
+ export {
11
+ AnnotationPopup,
12
+ CreateTooltipContent,
13
+ ConnectivityGraph,
14
+ CopyToClipboard,
15
+ DrawToolbar,
16
+ HelpModeDialog,
17
+ Tooltip,
18
+ TreeControls,
19
+ };
@@ -9,6 +9,7 @@ declare module 'vue' {
9
9
  export interface GlobalComponents {
10
10
  AnnotationPopup: typeof import('./components/Tooltip/AnnotationPopup.vue')['default']
11
11
  ConnectionDialog: typeof import('./components/DrawToolbar/ConnectionDialog.vue')['default']
12
+ ConnectivityGraph: typeof import('./components/ConnectivityGraph/ConnectivityGraph.vue')['default']
12
13
  CopyToClipboard: typeof import('./components/CopyToClipboard/CopyToClipboard.vue')['default']
13
14
  CreateTooltipContent: typeof import('./components/Tooltip/CreateTooltipContent.vue')['default']
14
15
  DrawToolbar: typeof import('./components/DrawToolbar/DrawToolbar.vue')['default']
@@ -20,6 +21,7 @@ declare module 'vue' {
20
21
  ElContainer: typeof import('element-plus/es')['ElContainer']
21
22
  ElHeader: typeof import('element-plus/es')['ElHeader']
22
23
  ElIcon: typeof import('element-plus/es')['ElIcon']
24
+ ElIconAim: typeof import('@element-plus/icons-vue')['Aim']
23
25
  ElIconArrowDown: typeof import('@element-plus/icons-vue')['ArrowDown']
24
26
  ElIconArrowUp: typeof import('@element-plus/icons-vue')['ArrowUp']
25
27
  ElIconClose: typeof import('@element-plus/icons-vue')['Close']
@@ -27,7 +29,11 @@ declare module 'vue' {
27
29
  ElIconDelete: typeof import('@element-plus/icons-vue')['Delete']
28
30
  ElIconEdit: typeof import('@element-plus/icons-vue')['Edit']
29
31
  ElIconFinished: typeof import('@element-plus/icons-vue')['Finished']
32
+ ElIconLock: typeof import('@element-plus/icons-vue')['Lock']
33
+ ElIconUnlock: typeof import('@element-plus/icons-vue')['Unlock']
30
34
  ElIconWarning: typeof import('@element-plus/icons-vue')['Warning']
35
+ ElIconZoomIn: typeof import('@element-plus/icons-vue')['ZoomIn']
36
+ ElIconZoomOut: typeof import('@element-plus/icons-vue')['ZoomOut']
31
37
  ElInput: typeof import('element-plus/es')['ElInput']
32
38
  ElMain: typeof import('element-plus/es')['ElMain']
33
39
  ElOption: typeof import('element-plus/es')['ElOption']