erd_map 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }