Skip to content

Commit 19bdd50

Browse files
committed
Automatic layout through instantiated strategy; code cleanup.
1 parent 9ffd77d commit 19bdd50

7 files changed

Lines changed: 436 additions & 156 deletions

smartgraph.properties

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# Vertex related configurations
99
#
1010
vertex.allow-user-move = true
11-
vertex.radius = 15
11+
vertex.radius = 15
1212
vertex.tooltip = true
1313
vertex.label = true
1414

@@ -22,7 +22,12 @@ edge.arrow = true
2222
# size in pixels (side of a triangle); only for directed graphs
2323
edge.arrowsize = 5
2424

25-
# (automatic) Force-directed layout related configurations
25+
# Force-directed layout related configurations
26+
#
27+
# Notice: deprecated since version 2. Force directed layout strategies are now
28+
# instantiated and can be swapped at runtime, per the Strategy design pattern.
29+
# The parameters are passed as arguments or one can use the default ones described
30+
# in the javadoc documentation.
2631
# -- You should experiment with different values for your
2732
# -- particular problem, knowing that not all will achieve
2833
# -- a stable state
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* The MIT License
3+
*
4+
* JavaFXSmartGraph | Copyright 2024 brunomnsilva@gmail.com
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package com.brunomnsilva.smartgraph.graphview;
26+
27+
import javafx.geometry.Point2D;
28+
29+
import java.util.Collection;
30+
31+
/**
32+
* A representation of a force directed layout "strategy" used during automatic layout of nodes in a {@link SmartGraphPanel}.
33+
* <br/>
34+
* Implementing classes should compute attractive and repulsive forces according to some algorithm.
35+
* Typically, if two graph nodes are not adjacent the force should be dominated by the repulsive force.
36+
* <br/>
37+
* See: <a href="https://en.wikipedia.org/wiki/Force-directed_graph_drawing">Wikipedia - Force-directed graph drawing</a>
38+
*
39+
* @param <V> The generic type of {@link SmartGraphVertexNode}, i.e., the nodes of a {@link SmartGraphPanel}.
40+
*/
41+
public abstract class ForceDirectedLayoutStrategy<V> {
42+
43+
/**
44+
* This method must compute forces between all graph nodes. Typically, repelling forces exist between all nodes (similarly to particles
45+
* with the same polarity), but attractive forces only exist between adjacent nodes (nodes that are connected).
46+
* <br/>
47+
* The default behavior is to iterate over all distinct pairs of nodes and compute
48+
* their combined forces (attractive and repulsive), by calling {@link #computeForceBetween(SmartGraphVertexNode, SmartGraphVertexNode, double, double)}.
49+
* <br/>
50+
* Other strategies that rely on some link of global metrics should override this method.
51+
*
52+
* @param nodes the current nodes of the graph
53+
* @param panelWidth the graph panel's width
54+
* @param panelHeight the graph panel's height
55+
*/
56+
public void computeForces(Collection<SmartGraphVertexNode<V>> nodes, double panelWidth, double panelHeight) {
57+
for (SmartGraphVertexNode<V> v : nodes) {
58+
for (SmartGraphVertexNode<V> w : nodes) {
59+
if(v == w) continue;
60+
61+
Point2D force = computeForceBetween(v, w, panelWidth, panelHeight);
62+
v.addForceVector(force.getX(), force.getY());
63+
}
64+
}
65+
}
66+
67+
/**
68+
* Computes a force vector between two nodes. The force vector is the result of the attractive and repulsive force between the two.
69+
*
70+
* @param v a node
71+
* @param w another node
72+
* @param panelWidth the graph panel's width
73+
* @param panelHeight the graph panel's height
74+
* @return the force vector
75+
*/
76+
protected abstract Point2D computeForceBetween(SmartGraphVertexNode<V> v, SmartGraphVertexNode<V> w, double panelWidth, double panelHeight);
77+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* The MIT License
3+
*
4+
* JavaFXSmartGraph | Copyright 2024 brunomnsilva@gmail.com
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package com.brunomnsilva.smartgraph.graphview;
26+
27+
import javafx.geometry.Point2D;
28+
29+
import java.util.Collection;
30+
31+
/**
32+
* An implementation of a spring system layout strategy with gravity towards the center.
33+
* <br/>
34+
* Applies the same spring system as {@link ForceDirectedSpringSystemLayoutStrategy} but with added gravitational pull
35+
* towards the center of the panel. Even with bipartite graphs, they will not repel each other to the edges of the panel.
36+
* <br/>
37+
* Parameters:
38+
* <br/>
39+
* Repulsive force (> 0): Recommended [1, 50]. Default 25. The strength of the repulsive force between nodes.
40+
* Higher values result in greater repulsion.
41+
* <br/>
42+
* Attraction force (> 0): Recommended [1, 5]. Default 3. The strength of the attractive force between connected nodes.
43+
* Higher values result in stronger attraction. Careful, because larger values may not produce stable states.
44+
* <br/>
45+
* Attraction scale (> 0): Recommended [1, ?]. Default 10. The scale factor for attraction.
46+
* It determines the effectiveness of the attraction force based on the distance between connected nodes.
47+
* <br/>
48+
* Acceleration: Mandatory ]0, 1]. Default 0.8. The acceleration factor applied to node movements.
49+
* Higher values result in faster movements.
50+
* <br/>
51+
* Gravity (> 0): Recommended ]0, 0.1]. Default 0.01. The higher the value, the more dominant the gravitation "pull" is.
52+
* Careful, because larger values may not produce stable states. The gravity force is applied after
53+
* the attraction/repulsive forces between nodes are computed. We don't want this force to dominate the layout placement.
54+
*
55+
* @param <V> The generic type of {@link SmartGraphVertexNode}, i.e., the nodes of a {@link SmartGraphPanel}.
56+
*/
57+
public class ForceDirectedSpringGravityLayoutStrategy<V> extends ForceDirectedSpringSystemLayoutStrategy<V> {
58+
59+
private final double gravity;
60+
61+
/**
62+
* Constructs a new instance of ForceDirectedSpringGravityLayoutStrategy with default parameters, namely:
63+
* <br/>
64+
* repulsiveForce = 25, attractionForce = 3, attractionScale = 10, acceleration = 0.8 and gravity = 0.01.
65+
*/
66+
public ForceDirectedSpringGravityLayoutStrategy() {
67+
super();
68+
this.gravity = 0.01;
69+
}
70+
71+
/**
72+
* Constructs a new instance of ForceDirectedSpringGravityLayoutStrategy with the specified parameters.
73+
*
74+
* @param repulsiveForce The strength of the repulsive force between nodes. Higher values result in greater repulsion.
75+
* @param attractionForce The strength of the attractive force between connected nodes. Higher values result in stronger attraction.
76+
* @param attractionScale The scale factor for attraction. It determines the effectiveness of the attraction force based on the distance between connected nodes.
77+
* @param acceleration The acceleration factor applied to node movements. Higher values result in faster movements.
78+
* @param gravity The strength of the gravity force applied to all nodes, attracting them towards the center of the layout area.
79+
*/
80+
public ForceDirectedSpringGravityLayoutStrategy(double repulsiveForce, double attractionForce, double attractionScale,
81+
double acceleration, double gravity) {
82+
super(repulsiveForce, attractionForce, attractionScale, acceleration);
83+
84+
Args.requireGreaterThan(gravity, "gravity", 0);
85+
Args.requireInRange(gravity, "gravity", 0, 1);
86+
this.gravity = gravity;
87+
}
88+
89+
@Override
90+
public void computeForces(Collection<SmartGraphVertexNode<V>> nodes, double panelWidth, double panelHeight) {
91+
// Attractive and repulsive forces
92+
for (SmartGraphVertexNode<V> v : nodes) {
93+
for (SmartGraphVertexNode<V> w : nodes) {
94+
if(v == w) continue;
95+
96+
Point2D force = computeForceBetween(v, w, panelWidth, panelHeight);
97+
v.addForceVector(force.getX(), force.getY());
98+
}
99+
}
100+
101+
// Gravitational pull towards the center for all nodes
102+
double centerX = panelWidth / 2;
103+
double centerY = panelHeight / 2;
104+
105+
for (SmartGraphVertexNode<V> v : nodes) {
106+
Point2D curPosition = v.getUpdatedPosition();
107+
Point2D forceCenter = new Point2D(centerX - curPosition.getX(), centerY - curPosition.getY())
108+
.multiply(gravity);
109+
110+
v.addForceVector(forceCenter.getX(), forceCenter.getY());
111+
}
112+
}
113+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* The MIT License
3+
*
4+
* JavaFXSmartGraph | Copyright 2024 brunomnsilva@gmail.com
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package com.brunomnsilva.smartgraph.graphview;
26+
27+
import javafx.geometry.Point2D;
28+
29+
/**
30+
* An implementation of a spring system layout strategy. This strategy allows to freely move the graph along
31+
* the panel, but if you have a bipartite graph, the sub-graphs will repel each other to the edges of the panel.
32+
* <br/>
33+
* Parameters:
34+
* <br/>
35+
* Repulsive force (> 0): Recommended [1, 50]. Default 25. The strength of the repulsive force between nodes.
36+
* Higher values result in greater repulsion.
37+
* <br/>
38+
* Attraction force (> 0): Recommended [1, 5]. Default 3. The strength of the attractive force between connected nodes.
39+
* Higher values result in stronger attraction. Careful, because larger values may not produce stable states.
40+
* <br/>
41+
* Attraction scale (> 0): Recommended [1, ?]. Default 10. The scale factor for attraction.
42+
* It determines the effectiveness of the attraction force based on the distance between connected nodes.
43+
* <br/>
44+
* Acceleration: Mandatory ]0, 1]. Default 0.8. The acceleration factor applied to node movements.
45+
* Higher values result in faster movements.
46+
*
47+
* @param <V> The generic type of {@link SmartGraphVertexNode}, i.e., the nodes of a {@link SmartGraphPanel}.
48+
*/
49+
public class ForceDirectedSpringSystemLayoutStrategy<V> extends ForceDirectedLayoutStrategy<V> {
50+
51+
private final double repulsiveForce;
52+
private final double attractionForce;
53+
private final double attractionScale;
54+
private final double acceleration;
55+
56+
/* just a scaling factor so all parameters are, at most, two-digit numbers. */
57+
private static final double A_THOUSAND = 1000;
58+
59+
/**
60+
* Constructs a new instance of ForceDirectedSpringGravityLayoutStrategy with default parameters, namely:
61+
* <br/>
62+
* repulsiveForce = 25, attractionForce = 3, attractionScale = 10 and acceleration = 0.8.
63+
*/
64+
public ForceDirectedSpringSystemLayoutStrategy() {
65+
this.repulsiveForce = 25;
66+
this.attractionForce = 3;
67+
this.attractionScale = 10;
68+
this.acceleration = 0.8;
69+
}
70+
71+
/**
72+
* Constructs a new instance of ForceDirectedSpringGravityLayoutStrategy with the specified parameters.
73+
*
74+
* @param repulsiveForce The strength of the repulsive force between nodes. Higher values result in greater repulsion.
75+
* @param attractionForce The strength of the attractive force between connected nodes. Higher values result in stronger attraction.
76+
* @param attractionScale The scale factor for attraction. It determines the effectiveness of the attraction force based on the distance between connected nodes.
77+
* @param acceleration The acceleration factor applied to node movements. Higher values result in faster movements.
78+
*/
79+
public ForceDirectedSpringSystemLayoutStrategy(double repulsiveForce, double attractionForce, double attractionScale, double acceleration) {
80+
Args.requireGreaterThan(repulsiveForce, "repulsiveForce", 0);
81+
Args.requireGreaterThan(attractionForce, "attractionForce", 0);
82+
Args.requireGreaterThan(attractionScale, "attractionScale", 0);
83+
Args.requireGreaterThan(acceleration, "acceleration", 0);
84+
Args.requireInRange(acceleration, "acceleration", 0, 1);
85+
86+
this.repulsiveForce = repulsiveForce;
87+
this.attractionForce = attractionForce;
88+
this.attractionScale = attractionScale;
89+
this.acceleration = acceleration;
90+
}
91+
92+
@Override
93+
protected Point2D computeForceBetween(SmartGraphVertexNode<V> v, SmartGraphVertexNode<V> w, double panelWidth, double panelHeight) {
94+
// The panel's width and height are not used in this strategy
95+
// This allows to freely move the graph to a particular region in the panel;
96+
// On the other hand, e.g., in a bipartite graph the two sub-graphs will repel each other to the edges of the panel
97+
98+
Point2D vPosition = v.getUpdatedPosition();
99+
Point2D wPosition = w.getUpdatedPosition();
100+
double distance = vPosition.distance(wPosition);
101+
Point2D forceDirection = wPosition.subtract(vPosition).normalize();
102+
103+
if (distance < 1) {
104+
distance = 1;
105+
}
106+
107+
// attractive force
108+
Point2D attraction;
109+
if(v.isAdjacentTo(w)) {
110+
double attraction_factor = attractionForce * Math.log(distance / attractionScale);
111+
attraction = forceDirection.multiply(attraction_factor);
112+
} else {
113+
attraction = new Point2D(0,0);
114+
}
115+
116+
// repelling force
117+
double repulsive_factor = repulsiveForce * A_THOUSAND / (distance * distance);
118+
Point2D repulsion = forceDirection.multiply(-repulsive_factor);
119+
120+
// combine forces
121+
Point2D totalForce = new Point2D(attraction.getX() + repulsion.getX(),
122+
attraction.getY() + repulsion.getY());
123+
124+
return totalForce.multiply(acceleration);
125+
}
126+
}

0 commit comments

Comments
 (0)