Skip to content

Commit eef6df4

Browse files
committed
Dependency: Improve layout
1 parent 6fc3242 commit eef6df4

2 files changed

Lines changed: 135 additions & 19 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<svg width="100%" height="100%" viewBox="-500 -250 1000 500"></svg>
1+
<svg width="100%" height="100%" viewBox="-750 -250 1500 500"></svg>

src/app/component/dependency-graph/dependency-graph.component.ts

Lines changed: 134 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { DataStore } from 'src/app/model/data-store';
77
export interface graphNodes {
88
id: string;
99
relativeLevel: number;
10+
relativeCount: number;
1011
}
1112

1213
export interface graphLinks {
@@ -25,9 +26,10 @@ export interface graph {
2526
styleUrls: ['./dependency-graph.component.css'],
2627
})
2728
export 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

Comments
 (0)