@things-factory/integration-ui 7.0.33 → 7.0.35

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.
Files changed (33) 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/package.json +2 -2
@@ -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
  }