@@ -6,6 +6,8 @@ import { DataStore } from 'src/app/model/data-store';
66
77export interface graphNodes {
88 id : string ;
9+ relativeLevel : number ;
10+ relativeCount : number ;
911}
1012
1113export interface graphLinks {
@@ -24,9 +26,10 @@ export interface graph {
2426 styleUrls : [ './dependency-graph.component.css' ] ,
2527} )
2628export class DependencyGraphComponent implements OnInit {
27- SIZE_OF_NODE : number = 10 ;
2829 COLOR_OF_LINK : string = 'black' ;
29- COLOR_OF_NODE : string = '#55bc55' ;
30+ COLOR_OF_NODE : string = '#66bb6a' ;
31+ COLOR_OF_PREDECESSOR : string = '#deeedeff' ;
32+ COLOR_OF_SUCCESSOR : string = '#fdfdfdff' ;
3033 BORDER_COLOR_OF_NODE : string = 'black' ;
3134 simulation : any ;
3235 dataStore : Partial < DataStore > = { } ;
@@ -39,7 +42,7 @@ export class DependencyGraphComponent implements OnInit {
3942
4043 ngOnInit ( ) : void {
4144 this . loader . load ( ) . then ( ( dataStore : DataStore ) => {
42- this . dataStore = this . dataStore ;
45+ this . dataStore = dataStore ;
4346 if ( ! dataStore . activityStore ) {
4447 throw Error ( 'No activity store loaded' ) ;
4548 }
@@ -57,8 +60,9 @@ export class DependencyGraphComponent implements OnInit {
5760 populateGraphWithActivitiesCurrentActivityDependsOn ( activity : Activity ) : void {
5861 this . addNode ( activity . name ) ;
5962 if ( activity . dependsOn ) {
63+ let i : number = 1 ;
6064 for ( const prececcor of activity . dependsOn ) {
61- this . addNode ( prececcor ) ;
65+ this . addNode ( prececcor , - 1 , i ++ ) ;
6266 this . graphData [ 'links' ] . push ( {
6367 source : prececcor ,
6468 target : activity . name ,
@@ -69,9 +73,10 @@ export class DependencyGraphComponent implements OnInit {
6973
7074 populateGraphWithActivitiesThatDependsOnCurrentActivity ( currentActivity : Activity ) {
7175 const all : Activity [ ] = this . dataStore . activityStore ?. getAllActivities ?.( ) ?? [ ] ;
76+ let i : number = 1 ;
7277 for ( const activity of all ) {
7378 if ( activity . dependsOn ?. includes ( currentActivity . name ) ) {
74- this . addNode ( activity . name ) ;
79+ this . addNode ( activity . name , 1 , i ++ ) ;
7580 this . graphData [ 'links' ] . push ( {
7681 source : currentActivity . name ,
7782 target : activity . name ,
@@ -80,40 +85,58 @@ export class DependencyGraphComponent implements OnInit {
8085 }
8186 }
8287
83- addNode ( activityName : string ) {
88+ addNode ( activityName : string , relativeLevel : number = 0 , relativeCount : number = 0 ) : void {
8489 if ( ! this . visited . has ( activityName ) ) {
85- this . graphData [ 'nodes' ] . push ( { id : activityName } ) ;
90+ this . graphData [ 'nodes' ] . push ( { id : activityName , relativeLevel , relativeCount } ) ;
8691 this . visited . add ( activityName ) ;
8792 }
8893 }
8994
9095 generateGraph ( activityName : string ) : void {
91- let svg = d3 . select ( 'svg' ) ,
92- width = + svg . attr ( 'width' ) ,
93- height = + svg . attr ( 'height' ) ;
96+ let svg = d3 . select ( 'svg' ) ;
9497
98+ // Now that rectWidth is set on each node, set up the simulation
9599 this . simulation = d3
96100 . forceSimulation ( )
97101 . force (
98102 'link' ,
99- d3 . forceLink ( ) . id ( function ( d : any ) {
100- return d . id ;
101- } )
103+ d3
104+ . forceLink ( )
105+ . id ( function ( d : any ) {
106+ return d . id ;
107+ } )
108+ . strength ( 0.1 )
109+ )
110+ . force (
111+ 'x' ,
112+ d3
113+ . forceX ( ( d : any ) => {
114+ let col : number = 7 ;
115+ return d . relativeLevel * Math . ceil ( d . relativeCount / col ) * 300 ;
116+ } )
117+ . strength ( 5 )
118+ )
119+ // .force('y', d3.forceY((d: any) => {
120+ // return d.relativeLevel * 30;
121+ // }).strength(10))
122+ . force ( 'charge' , d3 . forceManyBody ( ) . strength ( - 80 ) )
123+ . force (
124+ 'collide' ,
125+ d3 . forceCollide ( ( d : any ) => 30 )
102126 )
103- . force ( 'charge' , d3 . forceManyBody ( ) . strength ( - 12000 ) )
104- . force ( 'center' , d3 . forceCenter ( width / 2 , height / 2 ) ) ;
127+ . force ( 'center' , d3 . forceCenter ( 0 , 0 ) ) ;
105128
106129 svg
107130 . append ( 'defs' )
108131 . append ( 'marker' )
109132 . attr ( 'id' , 'arrowhead' )
110133 . attr ( 'viewBox' , '-0 -5 10 10' )
111- . attr ( 'refX' , 18 )
134+ . attr ( 'refX' , 0 )
112135 . attr ( 'refY' , 0 )
113136 . attr ( 'orient' , 'auto' )
114137 . attr ( 'markerWidth' , 13 )
115138 . attr ( 'markerHeight' , 13 )
116- . attr ( 'xoverflow ' , 'visible' )
139+ . attr ( 'overflow ' , 'visible' )
117140 . append ( 'svg:path' )
118141 . attr ( 'd' , 'M 0,-5 L 10 ,0 L 0,5' )
119142 . attr ( 'fill' , this . COLOR_OF_LINK )
@@ -139,28 +162,104 @@ export class DependencyGraphComponent implements OnInit {
139162 . append ( 'g' ) ;
140163 /* eslint-enable */
141164
142- var defaultNodeColor = this . COLOR_OF_NODE ;
143- node
144- . append ( 'circle' )
145- . attr ( 'r' , 10 )
146- . attr ( 'fill' , function ( d ) {
147- if ( d . id == activityName ) return 'yellow' ;
148- else return defaultNodeColor ;
149- } ) ;
165+ const rectHeight = 30 ;
166+ const rectRx = 10 ;
167+ const rectRy = 10 ;
168+ const padding = 20 ;
150169
170+ // Append text first so we can measure it
151171 node
152172 . append ( 'text' )
153- . attr ( 'dy' , '.35em' )
173+ . attr ( 'dy' , '0 .35em' )
154174 . attr ( 'text-anchor' , 'middle' )
155175 . text ( function ( d ) {
156176 return d . id ;
157177 } ) ;
158178
159- this . simulation . nodes ( this . graphData [ 'nodes' ] ) . on ( 'tick' , ticked ) ;
179+ // Now for each node, measure the text and insert a rect behind it
180+ const self = this ;
181+ node . each ( function ( this : SVGGElement , d : any ) {
182+ const textElem = d3 . select ( this ) . select ( 'text' ) . node ( ) as SVGTextElement ;
183+ let textWidth = 60 ; // fallback default
184+ if ( textElem && textElem . getBBox ) {
185+ textWidth = textElem . getBBox ( ) . width ;
186+ }
187+ const rectWidth = textWidth + padding ;
188+ d . rectWidth = rectWidth ; // Store for collision force
189+ // Insert rect before text
190+ d3 . select ( this )
191+ . insert ( 'rect' , 'text' )
192+ . attr ( 'x' , - rectWidth / 2 )
193+ . attr ( 'y' , - rectHeight / 2 )
194+ . attr ( 'width' , rectWidth )
195+ . attr ( 'height' , rectHeight )
196+ . attr ( 'rx' , rectRx )
197+ . attr ( 'ry' , rectRy )
198+ . attr ( 'fill' , ( d : any ) => {
199+ if ( d . relativeLevel == 0 ) return self . COLOR_OF_NODE ;
200+ return d . relativeLevel < 0 ? self . COLOR_OF_PREDECESSOR : self . COLOR_OF_SUCCESSOR ;
201+ } )
202+ . attr ( 'stroke' , self . BORDER_COLOR_OF_NODE )
203+ . attr ( 'stroke-width' , 1.5 ) ;
204+ } ) ;
205+
206+ this . simulation . nodes ( this . graphData [ 'nodes' ] ) . on ( 'tick' , ( ) => {
207+ self . rectCollide ( this . graphData [ 'nodes' ] ) ;
208+ ticked ( ) ;
209+ } ) ;
160210
161211 this . simulation . force ( 'link' ) . links ( this . graphData [ 'links' ] ) ;
162212
163213 function ticked ( ) {
214+ // Improved rectangle edge intersection for arrowhead placement
215+ function rectEdgeIntersection (
216+ sx : number ,
217+ sy : number ,
218+ tx : number ,
219+ ty : number ,
220+ rectWidth : number ,
221+ rectHeight : number ,
222+ offset : number = 0
223+ ) {
224+ // Rectangle centered at (tx, ty)
225+ const dx = tx - sx ;
226+ const dy = ty - sy ;
227+ const w = rectWidth / 2 ;
228+ const h = rectHeight / 2 ;
229+ // Parametric line: (sx, sy) + t*(dx, dy), t in [0,1]
230+ // Find smallest t in (0,1] where line crosses rectangle edge
231+ let tMin = 1 ;
232+ // Left/right sides
233+ if ( dx !== 0 ) {
234+ let t1 = ( w - ( sx - tx ) ) / dx ;
235+ let y1 = sy + t1 * dy ;
236+ if ( t1 > 0 && Math . abs ( y1 - ty ) <= h ) tMin = Math . min ( tMin , t1 ) ;
237+ let t2 = ( - w - ( sx - tx ) ) / dx ;
238+ let y2 = sy + t2 * dy ;
239+ if ( t2 > 0 && Math . abs ( y2 - ty ) <= h ) tMin = Math . min ( tMin , t2 ) ;
240+ }
241+ // Top/bottom sides
242+ if ( dy !== 0 ) {
243+ let t3 = ( h - ( sy - ty ) ) / dy ;
244+ let x3 = sx + t3 * dx ;
245+ if ( t3 > 0 && Math . abs ( x3 - tx ) <= w ) tMin = Math . min ( tMin , t3 ) ;
246+ let t4 = ( - h - ( sy - ty ) ) / dy ;
247+ let x4 = sx + t4 * dx ;
248+ if ( t4 > 0 && Math . abs ( x4 - tx ) <= w ) tMin = Math . min ( tMin , t4 ) ;
249+ }
250+ // Clamp tMin to [0,1]
251+ tMin = Math . max ( 0 , Math . min ( 1 , tMin ) ) ;
252+ // Move intersection back by 'offset' pixels along the direction from target to source
253+ let px = sx + dx * tMin ;
254+ let py = sy + dy * tMin ;
255+ if ( offset > 0 && ( dx !== 0 || dy !== 0 ) ) {
256+ const len = Math . sqrt ( dx * dx + dy * dy ) ;
257+ px -= ( dx / len ) * offset ;
258+ py -= ( dy / len ) * offset ;
259+ }
260+ return { x : px , y : py } ;
261+ }
262+
164263 link
165264 . attr ( 'x1' , function ( d : any ) {
166265 return d . source . x ;
@@ -169,9 +268,34 @@ export class DependencyGraphComponent implements OnInit {
169268 return d . source . y ;
170269 } )
171270 . attr ( 'x2' , function ( d : any ) {
271+ // If target has rectWidth, adjust arrow to edge minus offset
272+ if ( d . target . rectWidth ) {
273+ const pt = rectEdgeIntersection (
274+ d . source . x ,
275+ d . source . y ,
276+ d . target . x ,
277+ d . target . y ,
278+ d . target . rectWidth ,
279+ 30 ,
280+ 10 // rectHeight, offset
281+ ) ;
282+ return pt . x ;
283+ }
172284 return d . target . x ;
173285 } )
174286 . attr ( 'y2' , function ( d : any ) {
287+ if ( d . target . rectWidth ) {
288+ const pt = rectEdgeIntersection (
289+ d . source . x ,
290+ d . source . y ,
291+ d . target . x ,
292+ d . target . y ,
293+ d . target . rectWidth ,
294+ 30 ,
295+ 10
296+ ) ;
297+ return pt . y ;
298+ }
175299 return d . target . y ;
176300 } ) ;
177301
@@ -180,4 +304,56 @@ export class DependencyGraphComponent implements OnInit {
180304 } ) ;
181305 }
182306 }
307+
308+ /**
309+ * Custom rectangular collision force for D3 simulation.
310+ * Pushes nodes apart if their rectangles (boxes) overlap.
311+ * Assumes each node has .x, .y, and .rectWidth properties.
312+ * Uses a fixed rectHeight of 30 (half = 15).
313+ * @param nodes Array of node objects
314+ */
315+ rectCollide ( nodes : any [ ] ) {
316+ // Loop through all pairs of nodes
317+ let node ,
318+ nx1 ,
319+ nx2 ,
320+ ny1 ,
321+ ny2 ,
322+ other ,
323+ ox1 ,
324+ ox2 ,
325+ oy1 ,
326+ oy2 ,
327+ i ,
328+ n = nodes . length ;
329+ for ( i = 0 ; i < n ; ++ i ) {
330+ node = nodes [ i ] ;
331+ // Calculate bounding box for node
332+ nx1 = node . x - node . rectWidth / 2 ;
333+ nx2 = node . x + node . rectWidth / 2 ;
334+ ny1 = node . y - 15 ; // rectHeight / 2
335+ ny2 = node . y + 15 ;
336+ for ( let j = i + 1 ; j < n ; ++ j ) {
337+ other = nodes [ j ] ;
338+ // Calculate bounding box for other node
339+ ox1 = other . x - other . rectWidth / 2 ;
340+ ox2 = other . x + other . rectWidth / 2 ;
341+ oy1 = other . y - 15 ;
342+ oy2 = other . y + 15 ;
343+ // Check for overlap between rectangles
344+ if ( nx1 < ox2 && nx2 > ox1 && ny1 < oy2 && ny2 > oy1 ) {
345+ // Overlap detected, push nodes apart along the direction between them
346+ let dx = node . x - other . x || Math . random ( ) - 0.5 ;
347+ let dy = node . y - other . y || Math . random ( ) - 0.5 ;
348+ let l = Math . sqrt ( dx * dx + dy * dy ) ;
349+ let moveX = dx / l || 1 ;
350+ let moveY = dy / l || 1 ;
351+ node . x += moveX ;
352+ node . y += moveY ;
353+ other . x -= moveX ;
354+ other . y -= moveY ;
355+ }
356+ }
357+ }
358+ }
183359}
0 commit comments