erd_map 0.1.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,592 @@
1
+ class GraphManager {
2
+ constructor({
3
+ graphRenderer,
4
+ rectRenderer,
5
+ circleRenderer,
6
+ layoutProvider,
7
+ connectionsData,
8
+ layoutsByChunkData,
9
+ chunkedNodesData,
10
+ nodeWithCommunityIndexData,
11
+ selectingNodeLabel,
12
+ zoomModeToggle,
13
+ tapModeToggle,
14
+ displayTitleModeToggle,
15
+ nodeLabels,
16
+ plot,
17
+ windowObj,
18
+ }) {
19
+ this.graphRenderer = graphRenderer
20
+ this.rectRenderer = rectRenderer
21
+ this.circleRenderer = circleRenderer
22
+ this.nodeSource = this.graphRenderer.node_renderer.data_source
23
+ this.edgeSource = this.graphRenderer.edge_renderer.data_source
24
+ this.cardinalityDataSource = cardinalityDataSource
25
+ this.layoutProvider = layoutProvider
26
+ this.connections = JSON.parse(connectionsData)
27
+ this.layoutsByChunk = JSON.parse(layoutsByChunkData)
28
+ this.chunkedNodes = JSON.parse(chunkedNodesData)
29
+ this.nodeWithCommunityIndex = JSON.parse(nodeWithCommunityIndexData)
30
+ this.selectingNodeLabel = selectingNodeLabel
31
+ this.zoomModeToggle = zoomModeToggle
32
+ this.tapModeToggle = tapModeToggle
33
+ this.displayTitleModeToggle = displayTitleModeToggle
34
+ this.nodeLabels = nodeLabels
35
+ this.plot = plot
36
+ this.windowObj = windowObj
37
+ this.cbObj = cb_obj
38
+
39
+ this.#addNodeLabelsWithDelay()
40
+ this.resetPlot()
41
+ }
42
+
43
+ toggleTapped() {
44
+ this.#resetSearch()
45
+ const tappedNode = this.#findTappedNode()
46
+ this.#applyTap(tappedNode, { nodeTapped: true })
47
+ }
48
+
49
+ toggleHovered() {
50
+ const { selectedLayout, showingNodes } = this.#getShowingNodesAndSelectedLayout()
51
+ const { closestNodeName, minmumDistance } = this.#findClosestNodeWithMinmumDistance(selectedLayout, showingNodes)
52
+
53
+ const originalColors = this.#showTitleMode ? this.nodeSource.data["circle_original_color"] : this.nodeSource.data["rect_original_color"]
54
+ const originalRadius = this.nodeSource.data["original_radius"]
55
+
56
+ if (closestNodeName && minmumDistance < 0.005) {
57
+ // Emphasize nodes when find the closest node
58
+ const connectedNodes = (this.connections[closestNodeName] || []).concat([closestNodeName])
59
+ this.#nodesIndex.forEach((nodeName, i) => {
60
+ const isConnectedNode = selectedLayout[nodeName] && connectedNodes.includes(nodeName)
61
+ this.nodeSource.data["text_color"][i] = isConnectedNode ? HIGHLIGHT_TEXT_COLOR : BASIC_COLOR
62
+ this.nodeSource.data["text_outline_color"][i] = isConnectedNode ? BASIC_COLOR : null
63
+ })
64
+ this.nodeSource.data["fill_color"] = this.#nodesIndex.map((nodeName, i) => {
65
+ const isConnectedNode = selectedLayout[nodeName] && connectedNodes.includes(nodeName)
66
+ return isConnectedNode ? HIGHLIGHT_NODE_COLOR : originalColors[i]
67
+ })
68
+ this.nodeSource.data["radius"] = this.#nodesIndex.map((nodeName, i) => {
69
+ return nodeName === closestNodeName ? EMPTHASIS_NODE_SIZE : originalRadius[i]
70
+ })
71
+ this.edgeSource.data["line_color"] = this.#sourceNodes.map((start, i) => {
72
+ return [start, this.#targetNodes[i]].includes(closestNodeName) ? HIGHLIGHT_EDGE_COLOR : BASIC_COLOR
73
+ })
74
+ this.cardinalityDataSource.data["text_color"] = this.cardinalityDataSource.data["source"].map((sourceNodeName, i) => {
75
+ const targetNodeName = this.cardinalityDataSource.data["target"][i]
76
+ const isConnectedNode = (closestNodeName === sourceNodeName || closestNodeName === targetNodeName) &&
77
+ (selectedLayout[sourceNodeName] && connectedNodes.includes(sourceNodeName)) &&
78
+ (selectedLayout[targetNodeName] && connectedNodes.includes(targetNodeName))
79
+ return isConnectedNode ? HIGHLIGHT_EDGE_COLOR : BASIC_COLOR
80
+ })
81
+ } else {
82
+ // Revert to default states
83
+ this.nodeSource.data["radius"] = originalRadius
84
+ this.nodeSource.data["fill_color"] = originalColors
85
+ this.nodeSource.data["text_color"] = this.#nodesIndex.map(() => BASIC_COLOR)
86
+ this.nodeSource.data["text_outline_color"] = this.#nodesIndex.map(() => null)
87
+ this.edgeSource.data["line_color"] = this.#sourceNodes.map(() => BASIC_COLOR)
88
+ this.cardinalityDataSource.data["text_color"] = this.cardinalityDataSource.data["text_color"].map(() => BASIC_COLOR)
89
+ }
90
+
91
+ this.nodeSource.change.emit()
92
+ this.edgeSource.change.emit()
93
+ this.cardinalityDataSource.change.emit()
94
+ }
95
+
96
+ triggerZoom() {
97
+ if (this.#fixingZoom) { return }
98
+
99
+ if (this.windowObj.zoomTimeout !== undefined) { clearTimeout(this.windowObj.zoomTimeout) }
100
+
101
+ this.windowObj.zoomTimeout = setTimeout(() => { this.#handleZoom() }, 200)
102
+ }
103
+
104
+ zoomIn() {
105
+ const displayChunksCount = Math.min(this.#displayChunksCount + 1, this.chunkedNodes.length - 1)
106
+ this.#setDisplayChunksCount(displayChunksCount)
107
+ this.#executeZoom({ previousDisplayChunksCount: null })
108
+ }
109
+
110
+ zoomOut() {
111
+ const displayChunksCount = Math.max(this.#displayChunksCount - 1, 0)
112
+ this.#setDisplayChunksCount(displayChunksCount)
113
+ this.#executeZoom({ previousDisplayChunksCount: null })
114
+ }
115
+
116
+ toggleZoomMode() {
117
+ this.#setFixingZoom(!this.#fixingZoom)
118
+
119
+ if (this.#fixingZoom) {
120
+ this.zoomModeToggle.label = "Wheel mode: fix"
121
+ this.zoomModeToggle.button_type = "default"
122
+ } else {
123
+ this.zoomModeToggle.label = "Wheel mode: zoom"
124
+ this.zoomModeToggle.button_type = "warning"
125
+ this.#setSelectingNodeLabel(null)
126
+ this.selectingNodeLabel.change.emit()
127
+ }
128
+ }
129
+
130
+ toggleTapMode() {
131
+ if (this.#showingAssociation) {
132
+ this.tapModeToggle.label = "Tap mode: community"
133
+ this.tapModeToggle.button_type = "warning"
134
+ } else {
135
+ this.tapModeToggle.label = "Tap mode: association"
136
+ this.tapModeToggle.button_type = "default"
137
+ }
138
+ this.#setShowingAssociation(!this.#showingAssociation)
139
+ if (this.windowObj.selectingNode) {
140
+ this.#applyTap(this.windowObj.selectingNode, { nodeTapped: false })
141
+ }
142
+ }
143
+
144
+ toggleDisplayTitleMode() {
145
+ this.#setShowTitleMode(!this.#showTitleMode)
146
+ this.rectRenderer.visible = !this.#showTitleMode
147
+ this.circleRenderer.visible = this.#showTitleMode
148
+ this.#applyDisplayTitleMode()
149
+ }
150
+
151
+ reLayout() {
152
+ const minMax = {
153
+ minX: this.plot.x_range.start,
154
+ maxX: this.plot.x_range.end,
155
+ minY: this.plot.y_range.start,
156
+ maxY: this.plot.y_range.end,
157
+ isInsideDisplay(x, y) { return this.minX <= x && x <= this.maxX && this.minY <= y && y <= this.maxY },
158
+ randXY() {
159
+ return [
160
+ this.minX + Math.random() * (this.maxX - this.minX), // randX
161
+ this.minY + Math.random() * (this.maxY - this.minY), // randY
162
+ ]
163
+ },
164
+ distances() {
165
+ const averageRange = ((this.maxX - this.minX) + (this.maxY - this.minY)) / 2
166
+ return [
167
+ averageRange * 0.25, //minDistance
168
+ averageRange * 0.5, // maxDistance
169
+ ]
170
+ }
171
+ }
172
+ const placedPositions = []
173
+ const { showingNodes, selectedLayout } = this.#getShowingNodesAndSelectedLayout()
174
+ const newLayout = { ...selectedLayout }
175
+ showingNodes.forEach((nodeName) => {
176
+ const [currentX, currentY] = newLayout[nodeName] || this.#wholeLayout[nodeName]
177
+ if (minMax.isInsideDisplay(currentX, currentY)) {
178
+ const [newX, newY] = this.#findReLayoutXY(placedPositions, minMax)
179
+ newLayout[nodeName] = [newX, newY]
180
+ placedPositions.push([newX, newY])
181
+ }
182
+ })
183
+
184
+ this.#setShiftX(0)
185
+ this.#setShiftY(0)
186
+ this.#updateLayout(newLayout, null)
187
+ this.layoutProvider.graph_layout = newLayout
188
+ this.layoutProvider.change.emit()
189
+ }
190
+
191
+ searchNodes() {
192
+ this.#setSearchingTerm(searchBox.value.trim().toLowerCase().replaceAll("_", ""))
193
+ if (!this.#searchingTerm) { return this.#resetSearch() }
194
+
195
+ const { showingNodes, selectedLayout } = this.#getShowingNodesAndSelectedLayout()
196
+ this.#changeDisplayNodes(showingNodes)
197
+
198
+ this.layoutProvider.graph_layout = selectedLayout
199
+ this.layoutProvider.change.emit()
200
+
201
+ this.#setSelectingNodeLabel(this.#searchingTerm)
202
+ this.selectingNodeLabel.change.emit()
203
+ }
204
+
205
+ #applyTap(tappedNode, { nodeTapped }) {
206
+ if (this.#showingAssociation) {
207
+ if (this.windowObj.selectingNode === undefined && tappedNode === undefined) {
208
+ // When tap non-node area with non-selecting mode
209
+ // do nothing
210
+ } else if (nodeTapped && this.windowObj.selectingNode && (tappedNode === this.windowObj.selectingNode || tappedNode === undefined)) {
211
+ // When tap the same node or non-node area
212
+ this.#setSelectingNode(null)
213
+ this.#revertTapSelection()
214
+ } else {
215
+ // When tap new or another node
216
+ this.#setSelectingNode(tappedNode)
217
+ this.#setSelectedNode(tappedNode)
218
+ this.#applyTapSelection(tappedNode)
219
+ }
220
+ } else {
221
+ this.#setSelectingNode(tappedNode)
222
+ this.#setSelectedNode(tappedNode)
223
+ const { showingNodes } = this.#getShowingNodesAndSelectedLayout()
224
+ this.#changeDisplayNodes(showingNodes)
225
+ }
226
+ this.#setSelectingNodeLabel(this.windowObj.selectingNode)
227
+ this.selectingNodeLabel.change.emit()
228
+ }
229
+
230
+ resetPlot() {
231
+ this.#setShiftX(0)
232
+ this.#setShiftY(0)
233
+ this.#setStableRange(undefined)
234
+ this.#setDisplayChunksCount(0)
235
+ this.#setSelectingNode(null)
236
+ this.#setSelectedNode(null)
237
+ this.#setFixingZoom(!true) // Set the opposite value of toggled value
238
+ this.toggleZoomMode()
239
+ this.#resetSearch()
240
+
241
+ if (this.#showingAssociation === false) { this.toggleTapMode() }
242
+ if (this.#showTitleMode === false) { this.toggleDisplayTitleMode() }
243
+
244
+ const { showingNodes, selectedLayout } = this.#getShowingNodesAndSelectedLayout()
245
+ this.#changeDisplayNodes(showingNodes)
246
+
247
+ this.#updateLayout(this.layoutsByChunk[0])
248
+ const newLayout = { ...selectedLayout }
249
+ this.#nodesIndex.forEach((nodeName, i) => {
250
+ if (newLayout[nodeName] === undefined) {
251
+ newLayout[nodeName] = this.#wholeLayout[nodeName]
252
+ }
253
+ })
254
+ this.layoutProvider.graph_layout = newLayout
255
+ this.layoutProvider.change.emit()
256
+ }
257
+
258
+ #getShowingNodesAndSelectedLayout() {
259
+ let selectedLayout = this.#selectedLayout
260
+ let showingNodes = Object.keys(selectedLayout)
261
+
262
+ if (this.windowObj.selectingNode && this.#showingAssociation) {
263
+ selectedLayout = this.#wholeLayout
264
+ showingNodes = this.connections[this.windowObj.selectingNode] || []
265
+ showingNodes.push(this.windowObj.selectingNode)
266
+ } else if (this.windowObj.selectingNode) {
267
+ selectedLayout = this.#wholeLayout
268
+ const communityIndex = this.nodeWithCommunityIndex[this.windowObj.selectingNode]
269
+ showingNodes = Object.keys(this.nodeWithCommunityIndex).filter(node => this.nodeWithCommunityIndex[node] === communityIndex)
270
+ showingNodes.push(this.windowObj.selectingNode)
271
+ }
272
+ if (this.#searchingTerm) {
273
+ selectedLayout = this.#wholeLayout
274
+ showingNodes.forEach(nodeName => {
275
+ if (this.#selectedLayout[nodeName]) {
276
+ selectedLayout[nodeName] = this.#selectedLayout[nodeName]
277
+ }
278
+ })
279
+ const matchedNodes = this.#nodesIndex.filter(nodeName =>
280
+ nodeName.toLowerCase().includes(this.#searchingTerm)
281
+ )
282
+ showingNodes = showingNodes.concat(matchedNodes)
283
+ }
284
+
285
+ return { selectedLayout, showingNodes }
286
+ }
287
+
288
+ #findClosestNodeWithMinmumDistance(layout, candidateNodes) {
289
+ let closestNodeName
290
+ let minmumDistance = Infinity
291
+
292
+ this.#nodesIndex.forEach((nodeName, i) => {
293
+ if (!candidateNodes.includes(nodeName)) { return }
294
+
295
+ const xy = layout[nodeName] || this.#wholeLayout[nodeName]
296
+ const dx = xy[0] + this.#shiftX - this.#mouseX
297
+ const dy = xy[1] + this.#shiftY - this.#mouseY
298
+ const distance = dx * dx + dy * dy
299
+ if (distance < minmumDistance) {
300
+ minmumDistance = distance
301
+ closestNodeName = this.#nodesIndex[i]
302
+ }
303
+ })
304
+ return { closestNodeName, minmumDistance }
305
+ }
306
+
307
+ #changeDisplayNodes(showingNodes) {
308
+ this.nodeSource.data["alpha"] = this.#nodesIndex.map((nodeName) =>
309
+ showingNodes.includes(nodeName) ? VISIBLE : TRANSLUCENT
310
+ )
311
+ this.edgeSource.data["alpha"] = this.#sourceEdges.map((source, i) =>
312
+ showingNodes.includes(source) && showingNodes.includes(this.edgeSource.data["end"][i]) ? VISIBLE : TRANSLUCENT
313
+ )
314
+ this.cardinalityDataSource.data["alpha"] = this.cardinalityDataSource.data["source"].map((sourceNodeName, i) => {
315
+ const targetNodeName = this.cardinalityDataSource.data["target"][i]
316
+
317
+ if (showingNodes.includes(sourceNodeName) && showingNodes.includes(targetNodeName) && sourceNodeName !== targetNodeName) {
318
+ return VISIBLE
319
+ } else {
320
+ return 0
321
+ }
322
+ })
323
+
324
+ this.nodeSource.change.emit()
325
+ this.edgeSource.change.emit()
326
+ this.cardinalityDataSource.change.emit()
327
+ this.#applyDisplayTitleMode()
328
+ }
329
+
330
+ // @returns [nodesX[Array], nodesY[Array]]
331
+ #updateLayout(layout, centerNodeName) {
332
+ const nodesX = this.nodeSource.data["x"]
333
+ const nodesY = this.nodeSource.data["y"]
334
+ const applyShift = !!layout[centerNodeName]
335
+ if (applyShift) {
336
+ this.#setShiftX(this.#mouseX - layout[centerNodeName][0])
337
+ this.#setShiftY(this.#mouseY - layout[centerNodeName][1])
338
+ }
339
+ const shiftX = this.#shiftX
340
+ const shiftY = this.#shiftY
341
+ this.#nodesIndex.forEach((nodeName, i) => {
342
+ const [newX, newY] = layout[nodeName] || this.#wholeLayout[nodeName]
343
+ nodesX[i] = newX + shiftX
344
+ nodesY[i] = newY + shiftY
345
+ })
346
+
347
+ const cardinalityOffsetX = 0.2
348
+ const cardinalityOffsetY = 0.3
349
+ this.cardinalityDataSource.data["x"].forEach((_, i) => {
350
+ const sourceNodeName = this.cardinalityDataSource.data["source"][i]
351
+ const targetNodeName = this.cardinalityDataSource.data["target"][i]
352
+
353
+ const sourceLayout = layout[sourceNodeName] || this.#wholeLayout[sourceNodeName]
354
+ const targetLayout = layout[targetNodeName] || this.#wholeLayout[targetNodeName]
355
+
356
+ const sourceX = sourceLayout[0] + shiftX
357
+ const sourceY = sourceLayout[1] + shiftY
358
+ const targetX = targetLayout[0] + shiftX
359
+ const targetY = targetLayout[1] + shiftY
360
+ const vectorX = targetX - sourceX
361
+ const vectorY = targetY - sourceY
362
+ const length = Math.sqrt(vectorX * vectorX + vectorY * vectorY)
363
+
364
+ const isSourceNode = i % 2 === 0
365
+ this.cardinalityDataSource.data["x"][i] = isSourceNode ?
366
+ sourceX + (vectorX / length) * cardinalityOffsetX
367
+ : targetX - (vectorX / length) * cardinalityOffsetX
368
+ this.cardinalityDataSource.data["y"][i] = isSourceNode ?
369
+ sourceY + (vectorY / length) * cardinalityOffsetY
370
+ : targetY - (vectorY / length) * cardinalityOffsetY
371
+ })
372
+
373
+ this.nodeSource.change.emit()
374
+ this.edgeSource.change.emit()
375
+ this.cardinalityDataSource.change.emit()
376
+
377
+ return [nodesX, nodesY]
378
+ }
379
+
380
+ // @returns {string | undefined}
381
+ #findTappedNode() {
382
+ const { showingNodes } = this.#getShowingNodesAndSelectedLayout()
383
+ const selectedNodesIndices = this.cbObj.indices
384
+ const tappedNodeIndex = selectedNodesIndices.find((id) => showingNodes.includes(this.#nodesIndex[id]))
385
+ return tappedNodeIndex ? this.#nodesIndex[tappedNodeIndex] : undefined
386
+ }
387
+
388
+ #revertTapSelection() {
389
+ const { showingNodes, selectedLayout } = this.#getShowingNodesAndSelectedLayout()
390
+ this.#changeDisplayNodes(showingNodes)
391
+
392
+ const [nodesX, nodesY] = this.#updateLayout(selectedLayout, this.windowObj.selectedNode)
393
+ const newGraphLayout = {}
394
+ this.#nodesIndex.forEach((nodeName, i) => {
395
+ newGraphLayout[nodeName] = [nodesX[i], nodesY[i]]
396
+ })
397
+ this.layoutProvider.graph_layout = newGraphLayout
398
+ this.layoutProvider.change.emit()
399
+ }
400
+
401
+ #applyTapSelection(tappedNode) {
402
+ if (tappedNode === undefined) { return }
403
+
404
+ const connectedNodes = [...(this.connections[tappedNode] || []), tappedNode]
405
+ this.#changeDisplayNodes(connectedNodes)
406
+
407
+ const { selectedLayout } = this.#getShowingNodesAndSelectedLayout()
408
+ this.#updateLayout(selectedLayout, tappedNode)
409
+ this.#nodesIndex.forEach(nodeName => {
410
+ selectedLayout[nodeName][0] += this.#shiftX
411
+ selectedLayout[nodeName][1] += this.#shiftY
412
+ })
413
+ this.layoutProvider.graph_layout = selectedLayout
414
+ this.layoutProvider.change.emit()
415
+ }
416
+
417
+ #addNodeLabelsWithDelay() {
418
+ // Wait a sec because errors occur if we call add_layout multitime once
419
+ this.plot.add_layout(this.nodeLabels.titleModelLabel)
420
+ setTimeout(() => {
421
+ this.plot.add_layout(this.nodeLabels.foreignModelLabel)
422
+ setTimeout(() => {
423
+ this.plot.add_layout(this.nodeLabels.foreignColumnsLabel)
424
+ }, 100)
425
+ }, 100)
426
+ }
427
+
428
+ #applyDisplayTitleMode() {
429
+ this.nodeLabels.foreignModelLabel.visible = !this.#showTitleMode
430
+ this.nodeLabels.foreignColumnsLabel.visible = !this.#showTitleMode
431
+ this.nodeLabels.titleModelLabel.visible = this.#showTitleMode
432
+
433
+ if (this.#showTitleMode) {
434
+ this.displayTitleModeToggle.label = "Display mode: title"
435
+ this.circleRenderer.node_renderer.data_source.data["fill_color"] = this.nodeSource.data["circle_original_color"]
436
+ this.circleRenderer.node_renderer.data_source.data["alpha"] = this.nodeSource.data["alpha"]
437
+ this.circleRenderer.edge_renderer.data_source.data["alpha"] = this.edgeSource.data["alpha"]
438
+ this.circleRenderer.node_renderer.data_source.change.emit()
439
+ this.circleRenderer.edge_renderer.data_source.change.emit()
440
+ } else {
441
+ this.displayTitleModeToggle.label = "Display mode: foreign key"
442
+ this.rectRenderer.node_renderer.data_source.data["fill_color"] = this.nodeSource.data["rect_original_color"]
443
+ this.rectRenderer.node_renderer.data_source.data["alpha"] = this.nodeSource.data["alpha"]
444
+ this.rectRenderer.edge_renderer.data_source.data["alpha"] = this.edgeSource.data["alpha"]
445
+ this.rectRenderer.node_renderer.data_source.change.emit()
446
+ this.rectRenderer.edge_renderer.data_source.change.emit()
447
+ }
448
+ }
449
+
450
+ #findReLayoutXY(placedPositions, minMax) {
451
+ const maxAttemptCount = 100
452
+ const [minDistance, maxDistance] = minMax.distances()
453
+ for (let t = 0; t < maxAttemptCount; t++) {
454
+ const [randX, randY] = minMax.randXY()
455
+ const ngPosition = placedPositions.some(([existX, existY]) => {
456
+ const distanceX = existX - randX
457
+ const distanceY = existY - randY
458
+ const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY)
459
+ return distance < minDistance || distance > maxDistance
460
+ })
461
+ if (!ngPosition) { return [randX, randY] }
462
+ }
463
+ return minMax.randXY()
464
+ }
465
+
466
+ #handleZoom() {
467
+ const currentRange = this.cbObj.end - this.cbObj.start
468
+ if (this.windowObj.stableRange === undefined) { this.#setStableRange(currentRange) }
469
+ const previousDisplayChunksCount = this.#displayChunksCount
470
+ const stableRange = this.windowObj.stableRange
471
+
472
+ let displayChunksCount = this.#displayChunksCount
473
+
474
+ // distance < 0: Zoom in
475
+ // 0 < distance: Zoom out
476
+ let distance = currentRange - stableRange
477
+ const threshold = stableRange * 0.1
478
+ if (Math.abs(distance) >= Math.abs(threshold)) {
479
+ if (distance < 0) { // Zoom in
480
+ displayChunksCount = Math.min(displayChunksCount + 1, this.chunkedNodes.length - 1)
481
+ } else { // Zoom out
482
+ displayChunksCount = Math.max(displayChunksCount - 1, 0)
483
+ }
484
+ }
485
+ this.#setDisplayChunksCount(displayChunksCount)
486
+ this.#setStableRange(currentRange)
487
+ this.#executeZoom({ previousDisplayChunksCount })
488
+ }
489
+
490
+ #executeZoom({ previousDisplayChunksCount }) {
491
+ if (this.#displayChunksCount === previousDisplayChunksCount) { return }
492
+
493
+ this.#resetSearch()
494
+ this.#setSelectingNode(null)
495
+ this.#setSelectingNodeLabel(null)
496
+ this.selectingNodeLabel.change.emit()
497
+
498
+ const { showingNodes, selectedLayout } = this.#getShowingNodesAndSelectedLayout()
499
+ this.#changeDisplayNodes(showingNodes)
500
+
501
+ let closestNodeName
502
+ if (previousDisplayChunksCount === null) {
503
+ closestNodeName = null
504
+ } else {
505
+ // Find the closest node (to shift XY with updateLayout) only when have previousDisplayChunksCount
506
+ const showingNodes = [...Object.keys(this.layoutsByChunk[previousDisplayChunksCount]), this.windowObj.selectedNode].filter(node => node)
507
+ const closestNodeWithMinmumDistance = this.#findClosestNodeWithMinmumDistance(this.layoutsByChunk[previousDisplayChunksCount], showingNodes)
508
+ closestNodeName = closestNodeWithMinmumDistance['closestNodeName']
509
+ }
510
+
511
+ const [nodesX, nodesY] = this.#updateLayout(selectedLayout, closestNodeName)
512
+ const shiftedLayout = {}
513
+ this.#nodesIndex.forEach((nodeName, i) => {
514
+ shiftedLayout[nodeName] = [nodesX[i], nodesY[i]]
515
+ })
516
+ this.layoutProvider.graph_layout = shiftedLayout
517
+ this.layoutProvider.change.emit()
518
+ }
519
+
520
+ #resetSearch() {
521
+ this.#setSearchingTerm(null)
522
+
523
+ this.#setSelectingNodeLabel(null)
524
+ this.selectingNodeLabel.change.emit()
525
+
526
+ searchBox.value = ""
527
+ }
528
+
529
+ get #mouseX() { return this.cbObj.x || this.windowObj.lastMouseX || 0 }
530
+ get #mouseY() { return this.cbObj.y || this.windowObj.lastMouseY || 0 }
531
+ get #shiftX() { return this.windowObj.previousShiftX || 0 }
532
+ get #shiftY() { return this.windowObj.previousShiftY || 0 }
533
+ get #nodesIndex() { return this.nodeSource.data["index"] }
534
+ get #sourceNodes() { return this.edgeSource.data["start"] }
535
+ get #targetNodes() { return this.edgeSource.data["end"] }
536
+ get #sourceEdges() { return this.edgeSource.data["start"] }
537
+ get #displayChunksCount() {
538
+ if (this.windowObj.displayChunksCount === undefined) { this.#setDisplayChunksCount(0) }
539
+ return this.windowObj.displayChunksCount || 0
540
+ }
541
+ get #selectedLayout() {
542
+ const layout = this.layoutsByChunk[this.#displayChunksCount]
543
+ if (this.windowObj.selectedNode) {
544
+ layout[this.windowObj.selectedNode] = this.#wholeLayout[this.windowObj.selectedNode]
545
+ }
546
+ return layout
547
+ }
548
+ get #wholeLayout() { return this.layoutsByChunk.slice(-1)[0] }
549
+ get #fixingZoom() {
550
+ if (this.windowObj.fixingZoom === undefined) { this.#setFixingZoom(true) }
551
+ return this.windowObj.fixingZoom
552
+ }
553
+ get #showingAssociation() {
554
+ if (this.windowObj.showingAssociation === undefined) { this.#setShowingAssociation(true) }
555
+ return this.windowObj.showingAssociation
556
+ }
557
+ get #searchingTerm() { return this.windowObj.searchingTerm }
558
+ get #showTitleMode() {
559
+ if (this.windowObj.showTitleMode === undefined) { this.#setShowTitleMode(true) }
560
+ return this.windowObj.showTitleMode
561
+ }
562
+
563
+ // @param {Integer} value
564
+ #setDisplayChunksCount(value) { this.windowObj.displayChunksCount = value }
565
+ // @param {Float} value
566
+ #setStableRange(value) { this.windowObj.stableRange = value }
567
+ // @param {Float} value
568
+ #setShiftX(value) { this.windowObj.previousShiftX = value }
569
+ // @param {Float} value
570
+ #setShiftY(value) { this.windowObj.previousShiftY = value }
571
+ // @param { String } value
572
+ #setSelectingNode(value) { this.windowObj.selectingNode = value }
573
+ // @param {String} nodeName
574
+ #setSelectedNode(nodeName) { this.windowObj.selectedNode = nodeName }
575
+ // @param {Boolean} value
576
+ #setFixingZoom(value) { this.windowObj.fixingZoom = value }
577
+ // @param {String | null} value
578
+ #setSelectingNodeLabel(value) {
579
+ if (value) {
580
+ const mode = this.#showingAssociation ? "associations" : "community"
581
+ this.selectingNodeLabel.text = this.#searchingTerm ? `Searching: ${value}` : `Showing: ${value}'s ${mode}`
582
+ } else {
583
+ this.selectingNodeLabel.text = ""
584
+ }
585
+ }
586
+ // @param {String} value
587
+ #setSearchingTerm(value) { this.windowObj.searchingTerm = value }
588
+ // @param {Boolean} value
589
+ #setShowingAssociation(value) { this.windowObj.showingAssociation = value }
590
+ // @param {Boolean} value
591
+ #setShowTitleMode(value) { this.windowObj.showTitleMode = value }
592
+ }