@@ -7,6 +7,7 @@ import { DataStore } from 'src/app/model/data-store';
77export interface graphNodes {
88 id : string ;
99 relativeLevel : number ;
10+ relativeCount : number ;
1011}
1112
1213export interface graphLinks {
@@ -25,9 +26,10 @@ export interface graph {
2526 styleUrls : [ './dependency-graph.component.css' ] ,
2627} )
2728export class DependencyGraphComponent implements OnInit {
28- SIZE_OF_NODE : number = 10 ;
2929 COLOR_OF_LINK : string = 'black' ;
30- COLOR_OF_NODE : string = '#55bc55' ;
30+ COLOR_OF_NODE : string = '#66bb6a' ;
31+ COLOR_OF_PREDECESSOR : string = '#deeedeff' ;
32+ COLOR_OF_SUCCESSOR : string = '#fdfdfdff' ;
3133 BORDER_COLOR_OF_NODE : string = 'black' ;
3234 simulation : any ;
3335 dataStore : Partial < DataStore > = { } ;
@@ -58,8 +60,9 @@ export class DependencyGraphComponent implements OnInit {
5860 populateGraphWithActivitiesCurrentActivityDependsOn ( activity : Activity ) : void {
5961 this . addNode ( activity . name ) ;
6062 if ( activity . dependsOn ) {
63+ let i : number = 1 ;
6164 for ( const prececcor of activity . dependsOn ) {
62- this . addNode ( prececcor , - 1 ) ;
65+ this . addNode ( prececcor , - 1 , i ++ ) ;
6366 this . graphData [ 'links' ] . push ( {
6467 source : prececcor ,
6568 target : activity . name ,
@@ -70,9 +73,10 @@ export class DependencyGraphComponent implements OnInit {
7073
7174 populateGraphWithActivitiesThatDependsOnCurrentActivity ( currentActivity : Activity ) {
7275 const all : Activity [ ] = this . dataStore . activityStore ?. getAllActivities ?.( ) ?? [ ] ;
76+ let i : number = 1 ;
7377 for ( const activity of all ) {
7478 if ( activity . dependsOn ?. includes ( currentActivity . name ) ) {
75- this . addNode ( activity . name , 1 ) ;
79+ this . addNode ( activity . name , 1 , i ++ ) ;
7680 this . graphData [ 'links' ] . push ( {
7781 source : currentActivity . name ,
7882 target : activity . name ,
@@ -81,41 +85,45 @@ export class DependencyGraphComponent implements OnInit {
8185 }
8286 }
8387
84- addNode ( activityName : string , relativeLevel : number = 0 ) : void {
88+ addNode ( activityName : string , relativeLevel : number = 0 , relativeCount : number = 0 ) : void {
8589 if ( ! this . visited . has ( activityName ) ) {
86- this . graphData [ 'nodes' ] . push ( { id : activityName , relativeLevel : relativeLevel } ) ;
90+ this . graphData [ 'nodes' ] . push ( { id : activityName , relativeLevel, relativeCount } ) ;
8791 this . visited . add ( activityName ) ;
8892 }
8993 }
9094
9195 generateGraph ( activityName : string ) : void {
9296 let svg = d3 . select ( 'svg' ) ;
9397
98+
99+ // Now that rectWidth is set on each node, set up the simulation
94100 this . simulation = d3
95101 . forceSimulation ( )
96102 . force ( 'link' , d3 . forceLink ( ) . id ( function ( d : any ) {
97103 return d . id ;
98- } ) )
104+ } ) . strength ( 0.1 ) )
99105 . force ( 'x' , d3 . forceX ( ( d : any ) => {
100- return d . relativeLevel * 300 ;
101- } ) . strength ( 10 ) )
102- . force ( 'y' , d3 . forceY ( ( d : any ) => {
103- return d . relativeLevel * 30 ;
104- } ) . strength ( 10 ) )
105- . force ( 'charge' , d3 . forceManyBody ( ) . strength ( - 8000 ) )
106+ let col : number = 7 ;
107+ return d . relativeLevel * Math . ceil ( d . relativeCount / col ) * 300 ;
108+ } ) . strength ( 5 ) )
109+ // .force('y', d3.forceY((d: any) => {
110+ // return d.relativeLevel * 30;
111+ // }).strength(10))
112+ . force ( 'charge' , d3 . forceManyBody ( ) . strength ( - 80 ) )
113+ . force ( 'collide' , d3 . forceCollide ( ( d : any ) => 30 ) )
106114 . force ( 'center' , d3 . forceCenter ( 0 , 0 ) ) ;
107115
108116 svg
109117 . append ( 'defs' )
110118 . append ( 'marker' )
111119 . attr ( 'id' , 'arrowhead' )
112120 . attr ( 'viewBox' , '-0 -5 10 10' )
113- . attr ( 'refX' , 18 )
121+ . attr ( 'refX' , 0 )
114122 . attr ( 'refY' , 0 )
115123 . attr ( 'orient' , 'auto' )
116124 . attr ( 'markerWidth' , 13 )
117125 . attr ( 'markerHeight' , 13 )
118- . attr ( 'xoverflow ' , 'visible' )
126+ . attr ( 'overflow ' , 'visible' )
119127 . append ( 'svg:path' )
120128 . attr ( 'd' , 'M 0,-5 L 10 ,0 L 0,5' )
121129 . attr ( 'fill' , this . COLOR_OF_LINK )
@@ -143,7 +151,6 @@ export class DependencyGraphComponent implements OnInit {
143151
144152
145153
146- var defaultNodeColor = this . COLOR_OF_NODE ;
147154 const rectHeight = 30 ;
148155 const rectRx = 10 ;
149156 const rectRy = 10 ;
@@ -164,6 +171,7 @@ export class DependencyGraphComponent implements OnInit {
164171 textWidth = textElem . getBBox ( ) . width ;
165172 }
166173 const rectWidth = textWidth + padding ;
174+ d . rectWidth = rectWidth ; // Store for collision force
167175 // Insert rect before text
168176 d3 . select ( this )
169177 . insert ( 'rect' , 'text' )
@@ -173,16 +181,65 @@ export class DependencyGraphComponent implements OnInit {
173181 . attr ( 'height' , rectHeight )
174182 . attr ( 'rx' , rectRx )
175183 . attr ( 'ry' , rectRy )
176- . attr ( 'fill' , ( d : any ) => d . id == activityName ? 'yellow' : defaultNodeColor )
184+ . attr ( 'fill' , ( d : any ) => {
185+ if ( d . relativeLevel == 0 ) return self . COLOR_OF_NODE ;
186+ return d . relativeLevel < 0 ? self . COLOR_OF_PREDECESSOR : self . COLOR_OF_SUCCESSOR ;
187+ } )
177188 . attr ( 'stroke' , self . BORDER_COLOR_OF_NODE )
178189 . attr ( 'stroke-width' , 1.5 ) ;
179190 } ) ;
180191
181- this . simulation . nodes ( this . graphData [ 'nodes' ] ) . on ( 'tick' , ticked ) ;
192+ this . simulation . nodes ( this . graphData [ 'nodes' ] ) . on ( 'tick' , ( ) => {
193+ self . rectCollide ( this . graphData [ 'nodes' ] ) ;
194+ ticked ( ) ;
195+ } ) ;
182196
183197 this . simulation . force ( 'link' ) . links ( this . graphData [ 'links' ] ) ;
184198
185199 function ticked ( ) {
200+
201+
202+ // Improved rectangle edge intersection for arrowhead placement
203+ function rectEdgeIntersection ( sx : number , sy : number , tx : number , ty : number , rectWidth : number , rectHeight : number , offset : number = 0 ) {
204+ // Rectangle centered at (tx, ty)
205+ const dx = tx - sx ;
206+ const dy = ty - sy ;
207+ const w = rectWidth / 2 ;
208+ const h = rectHeight / 2 ;
209+ // Parametric line: (sx, sy) + t*(dx, dy), t in [0,1]
210+ // Find smallest t in (0,1] where line crosses rectangle edge
211+ let tMin = 1 ;
212+ // Left/right sides
213+ if ( dx !== 0 ) {
214+ let t1 = ( w - ( sx - tx ) ) / dx ;
215+ let y1 = sy + t1 * dy ;
216+ if ( t1 > 0 && Math . abs ( y1 - ty ) <= h ) tMin = Math . min ( tMin , t1 ) ;
217+ let t2 = ( - w - ( sx - tx ) ) / dx ;
218+ let y2 = sy + t2 * dy ;
219+ if ( t2 > 0 && Math . abs ( y2 - ty ) <= h ) tMin = Math . min ( tMin , t2 ) ;
220+ }
221+ // Top/bottom sides
222+ if ( dy !== 0 ) {
223+ let t3 = ( h - ( sy - ty ) ) / dy ;
224+ let x3 = sx + t3 * dx ;
225+ if ( t3 > 0 && Math . abs ( x3 - tx ) <= w ) tMin = Math . min ( tMin , t3 ) ;
226+ let t4 = ( - h - ( sy - ty ) ) / dy ;
227+ let x4 = sx + t4 * dx ;
228+ if ( t4 > 0 && Math . abs ( x4 - tx ) <= w ) tMin = Math . min ( tMin , t4 ) ;
229+ }
230+ // Clamp tMin to [0,1]
231+ tMin = Math . max ( 0 , Math . min ( 1 , tMin ) ) ;
232+ // Move intersection back by 'offset' pixels along the direction from target to source
233+ let px = sx + dx * tMin ;
234+ let py = sy + dy * tMin ;
235+ if ( offset > 0 && ( dx !== 0 || dy !== 0 ) ) {
236+ const len = Math . sqrt ( dx * dx + dy * dy ) ;
237+ px -= ( dx / len ) * offset ;
238+ py -= ( dy / len ) * offset ;
239+ }
240+ return { x : px , y : py } ;
241+ }
242+
186243 link
187244 . attr ( 'x1' , function ( d : any ) {
188245 return d . source . x ;
@@ -191,9 +248,26 @@ export class DependencyGraphComponent implements OnInit {
191248 return d . source . y ;
192249 } )
193250 . attr ( 'x2' , function ( d : any ) {
251+ // If target has rectWidth, adjust arrow to edge minus offset
252+ if ( d . target . rectWidth ) {
253+ const pt = rectEdgeIntersection (
254+ d . source . x , d . source . y ,
255+ d . target . x , d . target . y ,
256+ d . target . rectWidth , 30 , 10 // rectHeight, offset
257+ ) ;
258+ return pt . x ;
259+ }
194260 return d . target . x ;
195261 } )
196262 . attr ( 'y2' , function ( d : any ) {
263+ if ( d . target . rectWidth ) {
264+ const pt = rectEdgeIntersection (
265+ d . source . x , d . source . y ,
266+ d . target . x , d . target . y ,
267+ d . target . rectWidth , 30 , 10
268+ ) ;
269+ return pt . y ;
270+ }
197271 return d . target . y ;
198272 } ) ;
199273
@@ -202,4 +276,46 @@ export class DependencyGraphComponent implements OnInit {
202276 } ) ;
203277 }
204278 }
279+
280+ /**
281+ * Custom rectangular collision force for D3 simulation.
282+ * Pushes nodes apart if their rectangles (boxes) overlap.
283+ * Assumes each node has .x, .y, and .rectWidth properties.
284+ * Uses a fixed rectHeight of 30 (half = 15).
285+ * @param nodes Array of node objects
286+ */
287+ rectCollide ( nodes : any [ ] ) {
288+ // Loop through all pairs of nodes
289+ let node , nx1 , nx2 , ny1 , ny2 , other , ox1 , ox2 , oy1 , oy2 , i , n = nodes . length ;
290+ for ( i = 0 ; i < n ; ++ i ) {
291+ node = nodes [ i ] ;
292+ // Calculate bounding box for node
293+ nx1 = node . x - node . rectWidth / 2 ;
294+ nx2 = node . x + node . rectWidth / 2 ;
295+ ny1 = node . y - 15 ; // rectHeight / 2
296+ ny2 = node . y + 15 ;
297+ for ( let j = i + 1 ; j < n ; ++ j ) {
298+ other = nodes [ j ] ;
299+ // Calculate bounding box for other node
300+ ox1 = other . x - other . rectWidth / 2 ;
301+ ox2 = other . x + other . rectWidth / 2 ;
302+ oy1 = other . y - 15 ;
303+ oy2 = other . y + 15 ;
304+ // Check for overlap between rectangles
305+ if ( nx1 < ox2 && nx2 > ox1 && ny1 < oy2 && ny2 > oy1 ) {
306+ // Overlap detected, push nodes apart along the direction between them
307+ let dx = ( node . x - other . x ) || ( Math . random ( ) - 0.5 ) ;
308+ let dy = ( node . y - other . y ) || ( Math . random ( ) - 0.5 ) ;
309+ let l = Math . sqrt ( dx * dx + dy * dy ) ;
310+ let moveX = dx / l || 1 ;
311+ let moveY = dy / l || 1 ;
312+ node . x += moveX ;
313+ node . y += moveY ;
314+ other . x -= moveX ;
315+ other . y -= moveY ;
316+ }
317+ }
318+ }
319+ }
205320}
321+
0 commit comments