@things-factory/integration-ui 7.0.33 → 7.0.38

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. package/client/analysis/graph-data.ts +34 -0
  2. package/client/analysis/graph-viewer-old.ts +1097 -0
  3. package/client/analysis/graph-viewer-style.ts +5 -1
  4. package/client/analysis/graph-viewer.ts +245 -942
  5. package/client/analysis/node.ts +73 -0
  6. package/client/analysis/relationship.ts +19 -0
  7. package/client/analysis/utils.ts +41 -0
  8. package/client/pages/integration-analysis.ts +160 -71
  9. package/dist-client/analysis/graph-data.d.ts +36 -0
  10. package/dist-client/analysis/graph-data.js +2 -0
  11. package/dist-client/analysis/graph-data.js.map +1 -0
  12. package/dist-client/analysis/graph-viewer-old.d.ts +110 -0
  13. package/dist-client/analysis/graph-viewer-old.js +808 -0
  14. package/dist-client/analysis/graph-viewer-old.js.map +1 -0
  15. package/dist-client/analysis/graph-viewer-style.js +5 -1
  16. package/dist-client/analysis/graph-viewer-style.js.map +1 -1
  17. package/dist-client/analysis/graph-viewer.d.ts +25 -99
  18. package/dist-client/analysis/graph-viewer.js +189 -703
  19. package/dist-client/analysis/graph-viewer.js.map +1 -1
  20. package/dist-client/analysis/node.d.ts +4 -0
  21. package/dist-client/analysis/node.js +59 -0
  22. package/dist-client/analysis/node.js.map +1 -0
  23. package/dist-client/analysis/relationship.d.ts +4 -0
  24. package/dist-client/analysis/relationship.js +13 -0
  25. package/dist-client/analysis/relationship.js.map +1 -0
  26. package/dist-client/analysis/utils.d.ts +20 -0
  27. package/dist-client/analysis/utils.js +31 -0
  28. package/dist-client/analysis/utils.js.map +1 -0
  29. package/dist-client/pages/integration-analysis.d.ts +8 -3
  30. package/dist-client/pages/integration-analysis.js +153 -70
  31. package/dist-client/pages/integration-analysis.js.map +1 -1
  32. package/dist-client/tsconfig.tsbuildinfo +1 -1
  33. package/dist-server/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +3 -3
@@ -1,46 +1,42 @@
1
1
  import * as d3 from 'd3'
2
+ import { Node, Relationship, GraphData } from './graph-data'
2
3
 
3
4
  export class GraphViewer {
4
- public simulation
5
-
6
- private container
7
- private info
8
- private node
9
- private nodes
10
- private relationship
11
- private relationshipOutline
12
- private relationshipOverlay
13
- private relationshipText
14
- private relationships
15
- private selector
16
- private svg
17
- private svgNodes
18
- private svgRelationships
5
+ private svg: any
6
+ private svgNodes: any
7
+ private svgRelationships: any
8
+ private nodes: Node[] = []
9
+ private relationships: Relationship[] = []
10
+ private needZoomFit = true
11
+ private options: any
19
12
  private svgScale
20
- private svgTranslate: any
21
- private classes2colors = {}
22
- private justLoaded = false
23
- private numClasses = 0
24
- private options = {
25
- arrowSize: 4,
26
- colors: this.colors(),
27
- highlight: undefined,
28
- infoPanel: true,
29
- minCollision: undefined,
30
- graphData: undefined,
31
- dataUrl: undefined,
32
- nodeOutlineFillColor: undefined,
33
- nodeRadius: 25,
34
- relationshipColor: '#a5abb6',
35
- zoomFit: false
36
- } as any
13
+ private svgTranslate
14
+
15
+ public simulation: any
37
16
 
38
17
  constructor(_selector: any, _options: any) {
39
- this.init(_selector, _options)
18
+ this.options = {
19
+ arrowSize: 4,
20
+ colors: this.colors(),
21
+ highlight: undefined,
22
+ infoPanel: true,
23
+ minCollision: undefined,
24
+ graphData: undefined,
25
+ dataUrl: undefined,
26
+ nodeOutlineFillColor: undefined,
27
+ nodeRadius: 25,
28
+ relationshipColor: '#a5abb6',
29
+ zoomFit: false,
30
+ classes2colors: {},
31
+ numClasses: 0,
32
+ ..._options
33
+ }
34
+ this.init(_selector)
40
35
  }
41
36
 
42
- appendGraph(container) {
43
- this.svg = container
37
+ private init(selector: any) {
38
+ this.svg = d3
39
+ .select(selector)
44
40
  .append('svg')
45
41
  .attr('width', '100%')
46
42
  .attr('height', '100%')
@@ -63,83 +59,170 @@ export class GraphViewer {
63
59
  })
64
60
  )
65
61
  .on('dblclick.zoom', null)
66
- .append('g')
62
+ .append('g') // 그룹 요소 추가
67
63
  .attr('width', '100%')
68
64
  .attr('height', '100%')
69
65
 
70
- this.svgRelationships = this.svg.append('g').attr('class', 'relationships')
66
+ // Define arrow markers for graph links
67
+ this.svg
68
+ .append('defs')
69
+ .append('marker')
70
+ .attr('id', 'arrow')
71
+ .attr('viewBox', '0 -5 10 10')
72
+ .attr('refX', 10)
73
+ .attr('refY', 0)
74
+ .attr('markerWidth', 6)
75
+ .attr('markerHeight', 6)
76
+ .attr('orient', 'auto')
77
+ .append('path')
78
+ .attr('d', 'M0,-5L10,0L0,5')
79
+ .attr('fill', this.options.relationshipColor)
71
80
 
72
81
  this.svgNodes = this.svg.append('g').attr('class', 'nodes')
82
+ this.svgRelationships = this.svg.append('g').attr('class', 'relationships')
83
+ this.simulation = this.initSimulation()
84
+
85
+ if (this.options.graphData) {
86
+ this.updateWithGraphData(this.options.graphData)
87
+ }
73
88
  }
74
89
 
75
- appendImageToNode(node) {
76
- return node
77
- .append('image')
78
- .attr('height', d => {
79
- return this.icon(d) ? '24px' : '30px'
80
- })
81
- .attr('x', d => {
82
- return this.icon(d) ? '5px' : '-15px'
83
- })
84
- .attr('xlink:href', d => {
85
- return this.image(d)
86
- })
87
- .attr('y', d => {
88
- return this.icon(d) ? '5px' : '-16px'
90
+ initSimulation() {
91
+ const x = this.svg.node().parentElement.parentElement.clientWidth / 2
92
+ const y = this.svg.node().parentElement.parentElement.clientHeight / 2
93
+
94
+ var simulation = d3
95
+ .forceSimulation()
96
+ .force(
97
+ 'collide',
98
+ d3
99
+ .forceCollide()
100
+ .radius(d => {
101
+ return this.options.minCollision
102
+ })
103
+ .iterations(2)
104
+ )
105
+ .force('charge', d3.forceManyBody())
106
+ .force(
107
+ 'link',
108
+ d3.forceLink().id(d => {
109
+ return d.id
110
+ })
111
+ )
112
+ .force('center', d3.forceCenter(x, y))
113
+ .on('tick', () => {
114
+ this.tick()
89
115
  })
90
- .attr('width', d => {
91
- return this.icon(d) ? '24px' : '30px'
116
+ .on('end', () => {
117
+ if (this.options.zoomFit && this.needZoomFit) {
118
+ this.needZoomFit = false
119
+ this.zoomFit()
120
+ }
92
121
  })
122
+
123
+ return simulation
93
124
  }
94
125
 
95
- appendInfoPanel(container) {
96
- return container.append('div').attr('class', 'graph-info')
126
+ private tick() {
127
+ this.svgNodes.selectAll('.node').attr('transform', (d: any) => `translate(${d.x}, ${d.y})`)
128
+
129
+ this.svgRelationships
130
+ .selectAll('.relationship')
131
+ .attr('x1', (d: any) => this.calculateIntersection(d.source, d.target).x1)
132
+ .attr('y1', (d: any) => this.calculateIntersection(d.source, d.target).y1)
133
+ .attr('x2', (d: any) => this.calculateIntersection(d.source, d.target).x2)
134
+ .attr('y2', (d: any) => this.calculateIntersection(d.source, d.target).y2)
135
+
136
+ this.svgRelationships.selectAll('.relationship-text').attr('transform', (d: any) => {
137
+ const midX =
138
+ (this.calculateIntersection(d.source, d.target).x1 + this.calculateIntersection(d.source, d.target).x2) / 2
139
+ const midY =
140
+ (this.calculateIntersection(d.source, d.target).y1 + this.calculateIntersection(d.source, d.target).y2) / 2
141
+ return `translate(${midX}, ${midY})`
142
+ })
97
143
  }
98
144
 
99
- appendInfoElement(cls, isNode, property, value?: any) {
100
- var elem = this.info.append('a')
145
+ private calculateIntersection(source, target) {
146
+ const dx = target.x - source.x
147
+ const dy = target.y - source.y
148
+ const distance = Math.sqrt(dx * dx + dy * dy)
149
+ const ratio = (distance - this.options.nodeRadius) / distance
101
150
 
102
- elem
103
- .attr('href', '#')
104
- .attr('class', cls)
105
- .html('<strong>' + property + '</strong>' + (value ? ': ' + value : ''))
151
+ const x1 = source.x + dx * (this.options.nodeRadius / distance)
152
+ const y1 = source.y + dy * (this.options.nodeRadius / distance)
153
+ const x2 = target.x - dx * (this.options.nodeRadius / distance)
154
+ const y2 = target.y - dy * (this.options.nodeRadius / distance)
106
155
 
107
- if (!value) {
108
- elem
109
- .style('background-color', d => {
110
- return this.options.nodeOutlineFillColor
111
- ? this.options.nodeOutlineFillColor
112
- : isNode
113
- ? this.class2color(property)
114
- : this.defaultColor()
115
- })
116
- .style('border-color', d => {
117
- return this.options.nodeOutlineFillColor
118
- ? this.class2darkenColor(this.options.nodeOutlineFillColor)
119
- : isNode
120
- ? this.class2darkenColor(property)
121
- : this.defaultDarkenColor()
122
- })
123
- .style('color', d => {
124
- return this.options.nodeOutlineFillColor ? this.class2darkenColor(this.options.nodeOutlineFillColor) : '#fff'
125
- })
126
- }
156
+ return { x1, y1, x2, y2 }
127
157
  }
128
158
 
129
- appendInfoElementClass(cls, node) {
130
- this.appendInfoElement(cls, true, node)
159
+ private colors() {
160
+ return [
161
+ '#68bdf6', // light blue
162
+ '#6dce9e', // green #1
163
+ '#faafc2', // light pink
164
+ '#f2baf6', // purple
165
+ '#ff928c', // light red
166
+ '#fcea7e', // light yellow
167
+ '#ffc766', // light orange
168
+ '#405f9e', // navy blue
169
+ '#a5abb6', // dark gray
170
+ '#78cecb', // green #2,
171
+ '#b88cbb', // dark purple
172
+ '#ced2d9', // light gray
173
+ '#e84646', // dark red
174
+ '#fa5f86', // dark pink
175
+ '#ffab1a', // dark orange
176
+ '#fcda19', // dark yellow
177
+ '#797b80', // black
178
+ '#c9d96f', // pistachio
179
+ '#47991f', // green #3
180
+ '#70edee', // turquoise
181
+ '#ff75ea' // pink
182
+ ]
131
183
  }
132
184
 
133
- appendInfoElementProperty(cls, property, value) {
134
- this.appendInfoElement(cls, false, property, value)
185
+ updateWithGraphData(graphData: GraphData) {
186
+ this.nodes = graphData.results[0].data[0].graph.nodes
187
+ this.relationships = graphData.results[0].data[0].graph.relationships
188
+
189
+ this.relationships.forEach(rel => {
190
+ const sourceNode = this.nodes.find(node => node.id === rel.startNode)
191
+ const targetNode = this.nodes.find(node => node.id === rel.endNode)
192
+
193
+ if (!sourceNode || !targetNode) {
194
+ console.warn(`Node not found for relationship: ${rel.id}`)
195
+ return
196
+ }
197
+
198
+ rel.source = sourceNode
199
+ rel.target = targetNode
200
+ })
201
+
202
+ this.updateNodesAndRelationships()
135
203
  }
136
204
 
137
- appendInfoElementRelationship(cls, relationship) {
138
- this.appendInfoElement(cls, false, relationship)
205
+ private updateNodesAndRelationships() {
206
+ this.needZoomFit = true
207
+
208
+ this.svgNodes.selectAll('.node').remove()
209
+ this.svgRelationships.selectAll('.relationship').remove()
210
+ this.svgRelationships.selectAll('.relationship-text').remove()
211
+
212
+ this.appendNodesToGraph()
213
+ this.appendRelationshipsToGraph()
214
+
215
+ this.simulation.nodes(this.nodes)
216
+ ;(this.simulation.force('link') as d3.ForceLink<Node, Relationship>).links(this.relationships)
217
+
218
+ // 시뮬레이션을 강제로 재시작하여 레이아웃이 적절히 재정렬되도록 함
219
+ this.simulation.alpha(1).restart()
139
220
  }
140
221
 
141
- appendNode() {
142
- return this.node
222
+ private appendNodesToGraph() {
223
+ const nodeEnter = this.svgNodes
224
+ .selectAll('.node')
225
+ .data(this.nodes, (d: any) => d.id)
143
226
  .enter()
144
227
  .append('g')
145
228
  .attr('class', d => {
@@ -148,25 +231,10 @@ export class GraphViewer {
148
231
  classes = 'node',
149
232
  label = d.labels[0]
150
233
 
151
- if (this.icon(d)) {
234
+ if (d.icon) {
152
235
  classes += ' node-icon'
153
236
  }
154
237
 
155
- if (this.image(d)) {
156
- classes += ' node-image'
157
- }
158
-
159
- if (this.options.highlight) {
160
- for (i = 0; i < this.options.highlight.length; i++) {
161
- highlight = this.options.highlight[i]
162
-
163
- if (d.labels[0] === highlight.class && d.properties[highlight.property] === highlight.value) {
164
- classes += ' node-highlighted'
165
- break
166
- }
167
- }
168
- }
169
-
170
238
  return classes
171
239
  })
172
240
  .on('click', (event, d) => {
@@ -184,22 +252,20 @@ export class GraphViewer {
184
252
  }
185
253
  })
186
254
  .on('mouseenter', (event, d) => {
187
- if (this.info) {
188
- this.updateInfo(d)
189
- }
190
-
191
- if (typeof this.options.onNodeMouseEnter === 'function') {
192
- this.options.onNodeMouseEnter(d)
193
- }
255
+ event.target.dispatchEvent(
256
+ new CustomEvent('node-mouseenter', {
257
+ detail: d,
258
+ bubbles: true
259
+ })
260
+ )
194
261
  })
195
262
  .on('mouseleave', (event, d) => {
196
- if (this.info) {
197
- this.clearInfo()
198
- }
199
-
200
- if (typeof this.options.onNodeMouseLeave === 'function') {
201
- this.options.onNodeMouseLeave(d)
202
- }
263
+ event.target.dispatchEvent(
264
+ new CustomEvent('node-mouseleave', {
265
+ detail: d,
266
+ bubbles: true
267
+ })
268
+ )
203
269
  })
204
270
  .call(
205
271
  d3
@@ -208,871 +274,101 @@ export class GraphViewer {
208
274
  .on('drag', this.dragged.bind(this))
209
275
  .on('end', this.dragEnded.bind(this))
210
276
  )
211
- }
212
-
213
- appendNodeToGraph() {
214
- var n = this.appendNode()
215
277
 
216
- this.appendRingToNode(n)
217
- // this.appendBoxToNode(n)
218
- this.appendOutlineToNode(n)
219
-
220
- if (this.options.icons) {
221
- this.appendTextToNode(n)
222
- }
223
-
224
- if (this.options.images) {
225
- this.appendImageToNode(n)
226
- }
227
-
228
- return n
229
- }
230
-
231
- appendOutlineToNode(node) {
232
- const outline = node
278
+ nodeEnter
233
279
  .append('circle')
234
280
  .attr('class', 'outline')
235
281
  .attr('r', this.options.nodeRadius)
236
- .style('fill', d => {
237
- return this.options.nodeOutlineFillColor ? this.options.nodeOutlineFillColor : this.class2color(d.labels[0])
238
- })
239
- .style('stroke', d => {
240
- return this.options.nodeOutlineFillColor
241
- ? this.class2darkenColor(this.options.nodeOutlineFillColor)
242
- : this.class2darkenColor(d.labels[0])
243
- })
244
- .append('title')
245
- .text(d => {
246
- return this.toString(d)
247
- })
282
+ .style('fill', d => this.class2color(d.labels[0]))
283
+ .style('stroke', d => this.class2darkenColor(d.labels[0]))
284
+ .style('stroke-width', 2)
248
285
 
249
- node
286
+ nodeEnter
250
287
  .append('text')
251
- .attr('class', 'node-text')
288
+ .attr('class', 'text icon')
252
289
  .attr('x', 0)
253
- .attr('y', 52)
254
- .attr('text-anchor', 'middle') // 텍스트 중앙 정렬
255
- .attr('fill', 'black') // 텍스트 색상을 검은색으로 변경
256
- .text(d => d.text)
257
-
258
- return outline
259
- }
260
-
261
- appendBoxToNode(node) {
262
- const rect = node
263
- .append('rect')
264
- .attr('class', 'node-rect')
265
- .attr('width', 160) // 사각형의 너비
266
- .attr('height', 30) // 사각형의 높이
267
- .attr('x', -80) // 사각형의 중심을 기준으로 x 위치 조정
268
- .attr('y', -15) // 사각형의 중심을 기준으로 y 위치 조정
269
- .style('fill', d => {
270
- return this.options.nodeOutlineFillColor ? this.options.nodeOutlineFillColor : this.class2color(d.labels[0])
271
- })
272
- .style('stroke', d => {
273
- return this.options.nodeOutlineFillColor
274
- ? this.class2darkenColor(this.options.nodeOutlineFillColor)
275
- : this.class2darkenColor(d.labels[0])
276
- })
277
- .append('title')
278
- .text(d => {
279
- return this.toString(d)
280
- })
290
+ .attr('y', 0)
291
+ .attr('text-anchor', 'middle')
292
+ .attr('dominant-baseline', 'central')
293
+ .attr('font-family', 'Material Symbols Outlined')
294
+ .attr('font-size', '24px')
295
+ .attr('fill', '#000')
296
+ .text(d => this.getNodeIcon(d))
281
297
 
282
- node
298
+ nodeEnter
283
299
  .append('text')
284
- .attr('class', 'node-text')
285
- .attr('x', 0)
286
- .attr('y', 4)
287
- .attr('text-anchor', 'middle') // 텍스트 중앙 정렬
288
- .attr('fill', 'black') // 텍스트 색상을 검은색으로 변경
289
- .text(d => d.text)
290
-
291
- return rect
292
- }
293
-
294
- appendRingToNode(node) {
295
- return node
296
- .append('circle')
297
- .attr('class', 'ring')
298
- .attr('r', this.options.nodeRadius * 1.16)
299
- .append('title')
300
- .text(d => {
301
- return this.toString(d)
302
- })
300
+ .attr('dy', 40)
301
+ .attr('text-anchor', 'middle')
302
+ .text(d => d.properties.name || d.id)
303
303
  }
304
304
 
305
- appendTextToNode(node) {
306
- return node
307
- .append('text')
308
- .attr('fill', '#ffffff')
309
- .attr('pointer-events', 'none')
310
- .attr('text-anchor', 'middle')
311
- .attr('y', '24px')
312
- .attr('font-family', 'Material Symbols Outlined')
313
- .attr('font-size', '48px')
314
- .attr('text-anchor', 'middle')
315
- .attr('alignment-baseline', 'top')
316
- .text(d => this.icon(d))
305
+ stickNode(event: d3.event, d) {
306
+ d.fx = event.x
307
+ d.fy = event.y
317
308
  }
318
309
 
319
- appendRelationship() {
320
- return this.relationship
310
+ private appendRelationshipsToGraph() {
311
+ const relationshipEnter = this.svgRelationships
312
+ .selectAll('.relationship')
313
+ .data(
314
+ this.relationships.filter(rel => rel.source && rel.target),
315
+ d => d.id
316
+ )
321
317
  .enter()
322
318
  .append('g')
323
- .attr('class', 'relationship')
324
- .on('dblclick', (event, d) => {
325
- if (typeof this.options.onRelationshipDoubleClick === 'function') {
326
- this.options.onRelationshipDoubleClick(d)
327
- }
328
- })
329
- .on('mouseenter', (event, d) => {
330
- if (this.info) {
331
- this.updateInfo(d)
332
- }
333
- })
334
- }
319
+ .attr('class', 'relationship-group')
335
320
 
336
- appendOutlineToRelationship(r) {
337
- return r.append('path').attr('class', 'outline').attr('fill', '#a5abb6').attr('stroke', 'none')
338
- }
339
-
340
- appendOverlayToRelationship(r) {
341
- return r.append('path').attr('class', 'overlay')
342
- }
321
+ relationshipEnter
322
+ .append('line')
323
+ .attr('class', 'relationship')
324
+ .style('stroke', this.options.relationshipColor)
325
+ .style('stroke-width', 2)
326
+ .attr('marker-end', 'url(#arrow)')
343
327
 
344
- appendTextToRelationship(r) {
345
- return r
328
+ relationshipEnter
346
329
  .append('text')
347
- .attr('class', 'text')
330
+ .attr('class', 'relationship-text')
348
331
  .attr('fill', '#000000')
349
332
  .attr('font-size', '8px')
350
333
  .attr('pointer-events', 'none')
351
334
  .attr('text-anchor', 'middle')
352
- .text(d => {
353
- return d.type
354
- })
335
+ .text(d => d.type)
355
336
  }
356
337
 
357
- appendRelationshipToGraph() {
358
- var relationship = this.appendRelationship(),
359
- text = this.appendTextToRelationship(relationship),
360
- outline = this.appendOutlineToRelationship(relationship),
361
- overlay = this.appendOverlayToRelationship(relationship)
362
-
363
- return {
364
- outline: outline,
365
- overlay: overlay,
366
- relationship: relationship,
367
- text: text
368
- }
338
+ private getNodeIcon(d: Node): string {
339
+ return d.icon || ''
369
340
  }
370
341
 
371
- class2color(cls) {
372
- var color = this.classes2colors[cls]
373
-
374
- if (!color) {
375
- // color = this.options.colors[Math.min(numClasses, this.options.colors.length - 1)];
376
- color = this.options.colors[this.numClasses % this.options.colors.length]
377
- this.classes2colors[cls] = color
378
- this.numClasses++
342
+ private class2color(cls: string) {
343
+ if (!this.options.classes2colors[cls]) {
344
+ this.options.classes2colors[cls] = this.options.colors[this.options.numClasses % this.options.colors.length]
345
+ this.options.numClasses++
379
346
  }
380
-
381
- return color
347
+ return this.options.classes2colors[cls]
382
348
  }
383
349
 
384
- class2darkenColor(cls) {
350
+ private class2darkenColor(cls: string) {
385
351
  return d3.rgb(this.class2color(cls)).darker(1)
386
352
  }
387
353
 
388
- clearInfo() {
389
- this.info.html('')
390
- }
391
-
392
- color() {
393
- return this.options.colors[(this.options.colors.length * Math.random()) << 0]
394
- }
395
-
396
- colors() {
397
- // d3.schemeCategory10,
398
- // d3.schemeCategory20,
399
- return [
400
- '#68bdf6', // light blue
401
- '#6dce9e', // green #1
402
- '#faafc2', // light pink
403
- '#f2baf6', // purple
404
- '#ff928c', // light red
405
- '#fcea7e', // light yellow
406
- '#ffc766', // light orange
407
- '#405f9e', // navy blue
408
- '#a5abb6', // dark gray
409
- '#78cecb', // green #2,
410
- '#b88cbb', // dark purple
411
- '#ced2d9', // light gray
412
- '#e84646', // dark red
413
- '#fa5f86', // dark pink
414
- '#ffab1a', // dark orange
415
- '#fcda19', // dark yellow
416
- '#797b80', // black
417
- '#c9d96f', // pistacchio
418
- '#47991f', // green #3
419
- '#70edee', // turquoise
420
- '#ff75ea' // pink
421
- ]
422
- }
423
-
424
- contains(array, id) {
425
- var filter = array.filter(function (elem) {
426
- return elem.id === id
427
- })
428
-
429
- return filter.length > 0
430
- }
431
-
432
- defaultColor() {
433
- return this.options.relationshipColor
434
- }
435
-
436
- defaultDarkenColor() {
437
- return d3.rgb(this.options.colors[this.options.colors.length - 1]).darker(1)
438
- }
439
-
440
- dragEnded(event, d) {
441
- if (!event.active) {
442
- this.simulation.alphaTarget(0)
443
- }
444
-
445
- if (typeof this.options.onNodeDragEnd === 'function') {
446
- this.options.onNodeDragEnd(d)
447
- }
448
- }
449
-
450
- dragged(event, d) {
451
- this.stickNode(event, d)
452
- }
453
-
454
- dragStarted(event, d) {
455
- if (!event.active) {
456
- this.simulation.alphaTarget(0.3).restart()
457
- }
458
-
354
+ private dragStarted(event: any, d: Node) {
355
+ if (!event.active) this.simulation.alphaTarget(0.3).restart()
459
356
  d.fx = d.x
460
357
  d.fy = d.y
461
-
462
- if (typeof this.options.onNodeDragStart === 'function') {
463
- this.options.onNodeDragStart(d)
464
- }
465
358
  }
466
359
 
467
- extend(obj1, obj2) {
468
- var obj = {}
469
-
470
- this.merge(obj, obj1)
471
- this.merge(obj, obj2)
472
-
473
- return obj
474
- }
475
-
476
- icon(d) {
477
- return d.icon
478
- }
479
-
480
- image(d) {
481
- var i, imagesForLabel, img, imgLevel, label, labelPropertyValue, property, value
482
-
483
- if (this.options.images) {
484
- imagesForLabel = this.options.imageMap[d.labels[0]]
485
-
486
- if (imagesForLabel) {
487
- imgLevel = 0
488
-
489
- for (i = 0; i < imagesForLabel.length; i++) {
490
- labelPropertyValue = imagesForLabel[i].split('|')
491
-
492
- switch (labelPropertyValue.length) {
493
- case 3:
494
- value = labelPropertyValue[2]
495
- /* falls through */
496
- case 2:
497
- property = labelPropertyValue[1]
498
- /* falls through */
499
- case 1:
500
- label = labelPropertyValue[0]
501
- }
502
-
503
- if (
504
- d.labels[0] === label &&
505
- (!property || d.properties[property] !== undefined) &&
506
- (!value || d.properties[property] === value)
507
- ) {
508
- if (labelPropertyValue.length > imgLevel) {
509
- img = this.options.images[imagesForLabel[i]]
510
- imgLevel = labelPropertyValue.length
511
- }
512
- }
513
- }
514
- }
515
- }
516
-
517
- return img
518
- }
519
-
520
- init(_selector, _options) {
521
- this.merge(this.options, _options)
522
-
523
- if (this.options.icons) {
524
- this.options.showIcons = true
525
- }
526
-
527
- if (!this.options.minCollision) {
528
- this.options.minCollision = this.options.nodeRadius * 2
529
- }
530
-
531
- this.selector = _selector
532
-
533
- this.container = d3.select(this.selector)
534
-
535
- // this.container.attr('class', 'graph-container').html('')
536
-
537
- if (this.options.infoPanel) {
538
- this.info = this.appendInfoPanel(this.container)
539
- }
540
-
541
- this.appendGraph(this.container)
542
-
543
- this.simulation = this.initSimulation()
544
-
545
- if (this.options.graphData) {
546
- this.loadGraphData()
547
- } else if (this.options.dataUrl) {
548
- this.loadGraphDataFromUrl(this.options.dataUrl)
549
- } else {
550
- console.error('Error: both graphData and dataUrl are empty!')
551
- }
552
- }
553
-
554
- initSimulation() {
555
- const x = this.svg.node().parentElement.parentElement.clientWidth / 2
556
- const y = this.svg.node().parentElement.parentElement.clientHeight / 2
557
-
558
- var simulation = d3
559
- .forceSimulation()
560
- .force(
561
- 'collide',
562
- d3
563
- .forceCollide()
564
- .radius(d => {
565
- return this.options.minCollision
566
- })
567
- .iterations(2)
568
- )
569
- .force('charge', d3.forceManyBody())
570
- .force(
571
- 'link',
572
- d3.forceLink().id(d => {
573
- return d.id
574
- })
575
- )
576
- .force('center', d3.forceCenter(x, y))
577
- .on('tick', () => {
578
- this.tick()
579
- })
580
- .on('end', () => {
581
- if (this.options.zoomFit && !this.justLoaded) {
582
- this.justLoaded = true
583
- this.zoomFit(2)
584
- }
585
- })
586
-
587
- return simulation
588
- }
589
-
590
- loadGraphData() {
591
- this.nodes = []
592
- this.relationships = []
593
-
594
- this.updateWithGraphData(this.options.graphData)
595
- }
596
-
597
- loadGraphDataFromUrl(dataUrl) {
598
- this.nodes = []
599
- this.relationships = []
600
-
601
- d3.json(dataUrl, (error, data) => {
602
- if (error) {
603
- throw error
604
- }
605
-
606
- this.updateWithGraphData(data)
607
- })
608
- }
609
-
610
- merge(target, source) {
611
- Object.keys(source).forEach(property => {
612
- target[property] = source[property]
613
- })
614
- }
615
-
616
- graphDataToD3Data(data) {
617
- var graph = {
618
- nodes: [] as any[],
619
- relationships: [] as any[]
620
- }
621
-
622
- data.results.forEach(result => {
623
- result.data.forEach(data => {
624
- data.graph.nodes.forEach(node => {
625
- if (!this.contains(graph.nodes, node.id)) {
626
- graph.nodes.push(node)
627
- }
628
- })
629
-
630
- data.graph.relationships.forEach(function (relationship) {
631
- relationship.source = relationship.startNode
632
- relationship.target = relationship.endNode
633
- graph.relationships.push(relationship)
634
- })
635
-
636
- data.graph.relationships.sort(function (a, b) {
637
- if (a.source > b.source) {
638
- return 1
639
- } else if (a.source < b.source) {
640
- return -1
641
- } else {
642
- if (a.target > b.target) {
643
- return 1
644
- }
645
-
646
- if (a.target < b.target) {
647
- return -1
648
- } else {
649
- return 0
650
- }
651
- }
652
- })
653
-
654
- for (var i = 0; i < data.graph.relationships.length; i++) {
655
- if (
656
- i !== 0 &&
657
- data.graph.relationships[i].source === data.graph.relationships[i - 1].source &&
658
- data.graph.relationships[i].target === data.graph.relationships[i - 1].target
659
- ) {
660
- data.graph.relationships[i].linknum = data.graph.relationships[i - 1].linknum + 1
661
- } else {
662
- data.graph.relationships[i].linknum = 1
663
- }
664
- }
665
- })
666
- })
667
-
668
- return graph
669
- }
670
-
671
- randomD3Data(d, maxNodesToGenerate) {
672
- var data = {
673
- nodes: [] as any[],
674
- relationships: [] as any[]
675
- },
676
- i,
677
- label,
678
- node,
679
- numNodes = ((maxNodesToGenerate * Math.random()) << 0) + 1,
680
- relationship,
681
- s = this.size()
682
-
683
- for (i = 0; i < numNodes; i++) {
684
- label = this.randomLabel()
685
-
686
- node = {
687
- id: s.nodes + 1 + i,
688
- labels: [label],
689
- properties: {
690
- random: label
691
- },
692
- x: d.x,
693
- y: d.y
694
- }
695
-
696
- data.nodes[data.nodes.length] = node
697
-
698
- relationship = {
699
- id: s.relationships + 1 + i,
700
- type: label.toUpperCase(),
701
- startNode: d.id,
702
- endNode: s.nodes + 1 + i,
703
- properties: {
704
- from: Date.now()
705
- },
706
- source: d.id,
707
- target: s.nodes + 1 + i,
708
- linknum: s.relationships + 1 + i
709
- }
710
-
711
- data.relationships[data.relationships.length] = relationship
712
- }
713
-
714
- return data
715
- }
716
-
717
- randomLabel() {
718
- var icons = Object.keys(this.options.iconMap)
719
- return icons[(icons.length * Math.random()) << 0]
720
- }
721
-
722
- rotate(cx, cy, x, y, angle) {
723
- var radians = (Math.PI / 180) * angle,
724
- cos = Math.cos(radians),
725
- sin = Math.sin(radians),
726
- nx = cos * (x - cx) + sin * (y - cy) + cx,
727
- ny = cos * (y - cy) - sin * (x - cx) + cy
728
-
729
- return { x: nx, y: ny }
730
- }
731
-
732
- rotatePoint(c, p, angle) {
733
- return this.rotate(c.x, c.y, p.x, p.y, angle)
734
- }
735
-
736
- rotation(source, target) {
737
- return (Math.atan2(target.y - source.y, target.x - source.x) * 180) / Math.PI
738
- }
739
-
740
- size() {
741
- return {
742
- nodes: this.nodes.length,
743
- relationships: this.relationships.length
744
- }
745
- }
746
-
747
- stickNode(event, d) {
360
+ private dragged(event: any, d: Node) {
748
361
  d.fx = event.x
749
362
  d.fy = event.y
750
363
  }
751
364
 
752
- tick() {
753
- this.tickNodes()
754
- this.tickRelationships()
755
- }
756
-
757
- tickNodes() {
758
- if (this.node) {
759
- this.node.attr('transform', d => {
760
- return 'translate(' + d.x + ', ' + d.y + ')'
761
- })
762
- }
365
+ private dragEnded(event: any, d: Node) {
366
+ if (!event.active) this.simulation.alphaTarget(0)
367
+ d.fx = null
368
+ d.fy = null
763
369
  }
764
370
 
765
- tickRelationships() {
766
- if (this.relationship) {
767
- this.relationship.attr('transform', d => {
768
- var angle = this.rotation(d.source, d.target)
769
- return 'translate(' + d.source.x + ', ' + d.source.y + ') rotate(' + angle + ')'
770
- })
771
-
772
- this.tickRelationshipsTexts()
773
- this.tickRelationshipsOutlines()
774
- this.tickRelationshipsOverlays()
775
- }
776
- }
777
-
778
- tickRelationshipsOutlines() {
779
- const self = this
780
- this.relationship.each(function (this, relationship) {
781
- var rel = d3.select(this)
782
- var outline = rel.select('.outline'),
783
- text = rel.select('.text'),
784
- bbox = text.node().getBBox(),
785
- padding = 3
786
-
787
- outline.attr('d', d => {
788
- var center = { x: 0, y: 0 },
789
- angle = self.rotation(d.source, d.target),
790
- textBoundingBox = text.node().getBBox(),
791
- textPadding = 5,
792
- u = self.unitaryVector(d.source, d.target),
793
- textMargin = {
794
- x: (d.target.x - d.source.x - (textBoundingBox.width + textPadding) * u.x) * 0.5,
795
- y: (d.target.y - d.source.y - (textBoundingBox.width + textPadding) * u.y) * 0.5
796
- },
797
- n = self.unitaryNormalVector(d.source, d.target),
798
- rotatedPointA1 = self.rotatePoint(
799
- center,
800
- { x: 0 + (self.options.nodeRadius + 1) * u.x - n.x, y: 0 + (self.options.nodeRadius + 1) * u.y - n.y },
801
- angle
802
- ),
803
- rotatedPointB1 = self.rotatePoint(center, { x: textMargin.x - n.x, y: textMargin.y - n.y }, angle),
804
- rotatedPointC1 = self.rotatePoint(center, { x: textMargin.x, y: textMargin.y }, angle),
805
- rotatedPointD1 = self.rotatePoint(
806
- center,
807
- { x: 0 + (self.options.nodeRadius + 1) * u.x, y: 0 + (self.options.nodeRadius + 1) * u.y },
808
- angle
809
- ),
810
- rotatedPointA2 = self.rotatePoint(
811
- center,
812
- { x: d.target.x - d.source.x - textMargin.x - n.x, y: d.target.y - d.source.y - textMargin.y - n.y },
813
- angle
814
- ),
815
- rotatedPointB2 = self.rotatePoint(
816
- center,
817
- {
818
- x: d.target.x - d.source.x - (self.options.nodeRadius + 1) * u.x - n.x - u.x * self.options.arrowSize,
819
- y: d.target.y - d.source.y - (self.options.nodeRadius + 1) * u.y - n.y - u.y * self.options.arrowSize
820
- },
821
- angle
822
- ),
823
- rotatedPointC2 = self.rotatePoint(
824
- center,
825
- {
826
- x:
827
- d.target.x -
828
- d.source.x -
829
- (self.options.nodeRadius + 1) * u.x -
830
- n.x +
831
- (n.x - u.x) * self.options.arrowSize,
832
- y:
833
- d.target.y -
834
- d.source.y -
835
- (self.options.nodeRadius + 1) * u.y -
836
- n.y +
837
- (n.y - u.y) * self.options.arrowSize
838
- },
839
- angle
840
- ),
841
- rotatedPointD2 = self.rotatePoint(
842
- center,
843
- {
844
- x: d.target.x - d.source.x - (self.options.nodeRadius + 1) * u.x,
845
- y: d.target.y - d.source.y - (self.options.nodeRadius + 1) * u.y
846
- },
847
- angle
848
- ),
849
- rotatedPointE2 = self.rotatePoint(
850
- center,
851
- {
852
- x: d.target.x - d.source.x - (self.options.nodeRadius + 1) * u.x + (-n.x - u.x) * self.options.arrowSize,
853
- y: d.target.y - d.source.y - (self.options.nodeRadius + 1) * u.y + (-n.y - u.y) * self.options.arrowSize
854
- },
855
- angle
856
- ),
857
- rotatedPointF2 = self.rotatePoint(
858
- center,
859
- {
860
- x: d.target.x - d.source.x - (self.options.nodeRadius + 1) * u.x - u.x * self.options.arrowSize,
861
- y: d.target.y - d.source.y - (self.options.nodeRadius + 1) * u.y - u.y * self.options.arrowSize
862
- },
863
- angle
864
- ),
865
- rotatedPointG2 = self.rotatePoint(
866
- center,
867
- { x: d.target.x - d.source.x - textMargin.x, y: d.target.y - d.source.y - textMargin.y },
868
- angle
869
- )
870
-
871
- return (
872
- 'M ' +
873
- rotatedPointA1.x +
874
- ' ' +
875
- rotatedPointA1.y +
876
- ' L ' +
877
- rotatedPointB1.x +
878
- ' ' +
879
- rotatedPointB1.y +
880
- ' L ' +
881
- rotatedPointC1.x +
882
- ' ' +
883
- rotatedPointC1.y +
884
- ' L ' +
885
- rotatedPointD1.x +
886
- ' ' +
887
- rotatedPointD1.y +
888
- ' Z M ' +
889
- rotatedPointA2.x +
890
- ' ' +
891
- rotatedPointA2.y +
892
- ' L ' +
893
- rotatedPointB2.x +
894
- ' ' +
895
- rotatedPointB2.y +
896
- ' L ' +
897
- rotatedPointC2.x +
898
- ' ' +
899
- rotatedPointC2.y +
900
- ' L ' +
901
- rotatedPointD2.x +
902
- ' ' +
903
- rotatedPointD2.y +
904
- ' L ' +
905
- rotatedPointE2.x +
906
- ' ' +
907
- rotatedPointE2.y +
908
- ' L ' +
909
- rotatedPointF2.x +
910
- ' ' +
911
- rotatedPointF2.y +
912
- ' L ' +
913
- rotatedPointG2.x +
914
- ' ' +
915
- rotatedPointG2.y +
916
- ' Z'
917
- )
918
- })
919
- })
920
- }
921
-
922
- tickRelationshipsOverlays() {
923
- this.relationshipOverlay.attr('d', d => {
924
- var center = { x: 0, y: 0 },
925
- angle = this.rotation(d.source, d.target),
926
- n1 = this.unitaryNormalVector(d.source, d.target),
927
- n = this.unitaryNormalVector(d.source, d.target, 50),
928
- rotatedPointA = this.rotatePoint(center, { x: 0 - n.x, y: 0 - n.y }, angle),
929
- rotatedPointB = this.rotatePoint(
930
- center,
931
- { x: d.target.x - d.source.x - n.x, y: d.target.y - d.source.y - n.y },
932
- angle
933
- ),
934
- rotatedPointC = this.rotatePoint(
935
- center,
936
- { x: d.target.x - d.source.x + n.x - n1.x, y: d.target.y - d.source.y + n.y - n1.y },
937
- angle
938
- ),
939
- rotatedPointD = this.rotatePoint(center, { x: 0 + n.x - n1.x, y: 0 + n.y - n1.y }, angle)
940
-
941
- return (
942
- 'M ' +
943
- rotatedPointA.x +
944
- ' ' +
945
- rotatedPointA.y +
946
- ' L ' +
947
- rotatedPointB.x +
948
- ' ' +
949
- rotatedPointB.y +
950
- ' L ' +
951
- rotatedPointC.x +
952
- ' ' +
953
- rotatedPointC.y +
954
- ' L ' +
955
- rotatedPointD.x +
956
- ' ' +
957
- rotatedPointD.y +
958
- ' Z'
959
- )
960
- })
961
- }
962
-
963
- tickRelationshipsTexts() {
964
- this.relationshipText.attr('transform', d => {
965
- var angle = (this.rotation(d.source, d.target) + 360) % 360,
966
- mirror = angle > 90 && angle < 270,
967
- center = { x: 0, y: 0 },
968
- n = this.unitaryNormalVector(d.source, d.target),
969
- nWeight = mirror ? 2 : -3,
970
- point = {
971
- x: (d.target.x - d.source.x) * 0.5 + n.x * nWeight,
972
- y: (d.target.y - d.source.y) * 0.5 + n.y * nWeight
973
- },
974
- rotatedPoint = this.rotatePoint(center, point, angle)
975
-
976
- return 'translate(' + rotatedPoint.x + ', ' + rotatedPoint.y + ') rotate(' + (mirror ? 180 : 0) + ')'
977
- })
978
- }
979
-
980
- toString(d) {
981
- var s = d.labels ? d.labels[0] : d.type
982
-
983
- s += ' (<id>: ' + d.id
984
-
985
- Object.keys(d.properties).forEach(function (property) {
986
- s += ', ' + property + ': ' + JSON.stringify(d.properties[property])
987
- })
988
-
989
- s += ')'
990
-
991
- return s
992
- }
993
-
994
- unitaryNormalVector(source, target, newLength?: any) {
995
- var center = { x: 0, y: 0 },
996
- vector = this.unitaryVector(source, target, newLength)
997
-
998
- return this.rotatePoint(center, vector, 90)
999
- }
1000
-
1001
- unitaryVector(source, target, newLength?: any) {
1002
- var length =
1003
- Math.sqrt(Math.pow(target.x - source.x, 2) + Math.pow(target.y - source.y, 2)) / Math.sqrt(newLength || 1)
1004
-
1005
- return {
1006
- x: (target.x - source.x) / length,
1007
- y: (target.y - source.y) / length
1008
- }
1009
- }
1010
-
1011
- updateWithD3Data(d3Data) {
1012
- this.updateNodesAndRelationships(d3Data.nodes, d3Data.relationships)
1013
- }
1014
-
1015
- updateWithGraphData(graphData) {
1016
- var d3Data = this.graphDataToD3Data(graphData)
1017
- this.updateWithD3Data(d3Data)
1018
- }
1019
-
1020
- updateInfo(d) {
1021
- this.clearInfo()
1022
-
1023
- if (d.labels) {
1024
- this.appendInfoElementClass('class', d.labels[0])
1025
- } else {
1026
- this.appendInfoElementRelationship('class', d.type)
1027
- }
1028
-
1029
- this.appendInfoElementProperty('property', '&lt;id&gt;', d.id)
1030
-
1031
- Object.keys(d.properties).forEach(property => {
1032
- this.appendInfoElementProperty('property', property, JSON.stringify(d.properties[property]))
1033
- })
1034
- }
1035
-
1036
- updateNodes(n) {
1037
- Array.prototype.push.apply(this.nodes, n)
1038
-
1039
- this.node = this.svgNodes.selectAll('.node').data(this.nodes, d => {
1040
- return d.id
1041
- })
1042
- var nodeEnter = this.appendNodeToGraph()
1043
- this.node = nodeEnter.merge(this.node)
1044
- }
1045
-
1046
- updateNodesAndRelationships(n, r) {
1047
- this.updateRelationships(r)
1048
- this.updateNodes(n)
1049
-
1050
- this.simulation.nodes(this.nodes)
1051
- this.simulation.force('link').links(this.relationships)
1052
- }
1053
-
1054
- updateRelationships(r) {
1055
- Array.prototype.push.apply(this.relationships, r)
1056
-
1057
- this.relationship = this.svgRelationships.selectAll('.relationship').data(this.relationships, d => {
1058
- return d.id
1059
- })
1060
-
1061
- var relationshipEnter = this.appendRelationshipToGraph()
1062
-
1063
- this.relationship = relationshipEnter.relationship.merge(this.relationship)
1064
-
1065
- this.relationshipOutline = this.svg.selectAll('.relationship .outline')
1066
- this.relationshipOutline = relationshipEnter.outline.merge(this.relationshipOutline)
1067
-
1068
- this.relationshipOverlay = this.svg.selectAll('.relationship .overlay')
1069
- this.relationshipOverlay = relationshipEnter.overlay.merge(this.relationshipOverlay)
1070
-
1071
- this.relationshipText = this.svg.selectAll('.relationship .text')
1072
- this.relationshipText = relationshipEnter.text.merge(this.relationshipText)
1073
- }
1074
-
1075
- zoomFit(transitionDuration) {
371
+ zoomFit() {
1076
372
  var bounds = this.svg.node().getBBox(),
1077
373
  parent = this.svg.node().parentElement.parentElement,
1078
374
  fullWidth = parent.clientWidth,
@@ -1094,4 +390,11 @@ export class GraphViewer {
1094
390
  'translate(' + this.svgTranslate[0] + ', ' + this.svgTranslate[1] + ') scale(' + this.svgScale + ')'
1095
391
  )
1096
392
  }
393
+
394
+ size() {
395
+ return {
396
+ nodes: this.nodes.length,
397
+ relationships: this.relationships.length
398
+ }
399
+ }
1097
400
  }