Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions demo/forcedirectedgraph.generate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
window.demoDescription = 'Force-directed Graph generation';

var space = new CanvasSpace('pt').setup({
bgcolor: '#e6e6e6'
});
var form = new Form(space);
var graph = new ForceDirectedGraph(space.size.x, space.size.y);

for (var i = 0; i < 50; i++) {
// Could be new Vector as well (or anything that inherits from Vector)
graph.addVertex(new Vertex());
}
for (var i = 1; i < 50; i++) {
graph.addEdge(new Edge(graph.vertices[i - 1], graph.vertices[i]));
}
graph.addEdge(new Edge(graph.vertices[0], graph.vertices[graph.vertices.length - 1]));
graph.randomize();
graph.generate();

space.add({
animate: function(time, dt) {
form.stroke(false);
form.points(graph.vertices);
form.stroke('#000');
form.lines(graph.edges);
}
});

space.play();
9 changes: 5 additions & 4 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
<link href='http://fonts.googleapis.com/css?family=Roboto+Slab:400,700,300' rel='stylesheet' type='text/css'>

<meta charset="UTF-8">
<script type="text/javascript" src="../dist/pt.min.js"></script>
<!--<script type="text/javascript" src="../dist/pt.min.js"></script>-->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<!--<script type="text/javascript" src="../dist/core/pt-core.js"></script>-->
<!--<script type="text/javascript" src="../dist/extend/pt-extend.js"></script>-->
<script type="text/javascript" src="../dist/core/pt-core.js"></script>
<script type="text/javascript" src="../dist/extend/pt-extend.js"></script>

<style>

Expand Down Expand Up @@ -366,6 +366,7 @@
<p class="demo" data-src="samplepoints.poisson"><span>samplepoints.</span><strong>poisson</strong></p>
<p class="demo" data-src="delaunay.generate"><span>delaunay.</span><strong>generate</strong></p>
<p class="demo" data-src="shaping.linear"><span>shaping.</span><strong>linear</strong></p>
<p class="demo" data-src="forcedirectedgraph.generate"><span>forcedirectedgraph.</span><strong>generate</strong></p>
<p class="demo" data-src="mobile.multiTouch"><span>mobile.</span><strong>multiTouch</strong></p>
</div>
<div id="hint"></div>
Expand Down Expand Up @@ -411,4 +412,4 @@
</script>

</body>
</html>
</html>
3 changes: 2 additions & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ var coreFiles = coreElems.map(function(n) { return path.src.core+n+".coffee"; }

var extendElems = [
"Easing", "GridCascade", "ParticleEmitter", "ParticleField", "QuadTree",
"SamplePoints", "StripeBound", "UI", "Noise", "Delaunay", "Shaping"
"SamplePoints", "StripeBound", "UI", "Noise", "Delaunay", "Shaping",
"ForceDirectedGraph", "Vertex", "Edge"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should add Vertex and Edge as separate classes right now, unless they are useful enough on their own.

];
var extendFiles = extendElems.map(function(n) { return path.src.extend+n+".coffee"; } );

Expand Down
154 changes: 154 additions & 0 deletions src/coffee/extend/ForceDirectedGraph.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
Function::property = (prop, desc) ->
Object.defineProperty @prototype, prop, desc


# Hacky Edge implementation
class Edge
constructor: (source, target) ->
Copy link
Copy Markdown
Owner

@williamngan williamngan Oct 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a new class with source and target, I wonder if this can be just a Pair or Line

eg: var edge = new Line( source ).to( target ) ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Edge implementation holds a reference to the source and target. The Line/Point implementation currently just copies the coordinates from source/target.

Two solutions I can currently think of:

  • We change Graph.addEdge(Edge edge) to Graph.addEdge(Point, Point) or Graph.addEdge([Point, Point])
  • Let Pair also accept references to Point.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Then I think either Graph.addEdge(P, P) or using an Edge class will make sense. Changing Pair to accept references seems a bit more dangerous :)

@source = source
@target = target

@property 'p1',
get: ->
return {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be simplified as @target.clone()

x: @target.x,
y: @target.y,
z: @target.z
}

@property 'x',
get: ->
return @source.x

@property 'y',
get: ->
return @source.y

@property 'z',
get: ->
return @source.z

clone: () ->
return new Edge(@source.clone(), @target.clone())


class Vertex extends Vector
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be a Pair, or do you prefer the more semantic name displacement instead of p1?

constructor: () ->
super
@displacement = new Vector()

class Graph
constructor: () ->
@vertices = []
@edges = []

addVertex: (vertex) ->
@vertices.push(vertex)
return @

addEdge: (edge) ->
@edges.push(edge)
return @


class ForceDirectedGraph extends Graph
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think if we simply make this as Graph, and then rename the generate() to forceDirected() ?

# Fruchterman & Reingold algorithm: http://citeseer.ist.psu.edu/viewdoc/download?doi=10.1.1.13.8444&rep=rep1&type=pdf
# Walshaw's algorithm: http://jgaa.info/accepted/2003/Walshaw2003.7.3.pdf

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reading all the papers! 👍

# ## Create a new force-directed graph. Vertices and Edges are stored in `this.vertices` and `this.edges`, respectively.
# @return new ForceDirectedGraph object
constructor: (width, height) ->
super()
@width = width
@height = height
@frames = []
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@frames is not used, right?


_fa: (x, k) ->
Copy link
Copy Markdown
Owner

@williamngan williamngan Oct 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May want to move _fa, _fa, and _cool inside generate, since they are only used inside it.

return x * x / k;

_fr: (x, k) ->
return k * k / x;

_cool: (lambda, temperature) ->
return lambda * temperature

# ## Calculate new vertex/edge positions using a force-directed graph algorithm
# @return this ForceDirectedGraph object
generate: () ->
lambda = 0.9 # From Walshaw paper
iterations = 50 # From F&R paper
temperature = 0.1 * Math.sqrt(@width * @height) # From F&R paper
C = 0.2 # From Walshaw paper
k = C * Math.sqrt(@width * @height / @vertices.length)

for i in [0...iterations] by 1

# Caluclate repulsive forces
for v in @vertices

# Each vertex has a position and a displacement vector
v.displacement = new Vector()

for u in @vertices when u isnt v
delta = v.$subtract(u)
deltaMag = delta.magnitude()

if (deltaMag == 0)
deltaMag = 0.01; # If two different vertices are in the same position

v.displacement.add(
delta.$divide(deltaMag)
.multiply(@_fr(deltaMag, k))
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would prefer moving this to a single line: $divide(...).multiply(...)

)

# Calculate attractive forces
for e in @edges
delta = e.source.$subtract(e.target)
deltaMag = delta.magnitude()

if (deltaMag == 0)
deltaMag = 0.01;

e.source.displacement.subtract(
delta.$divide(deltaMag)
.multiply(@_fa(deltaMag, k))
)
e.target.displacement.add(
delta.$divide(deltaMag)
.multiply(@_fa(deltaMag, k))
)

# Limit the maximum displacement to the temperature
# and then make sure the vertices don't fall ourside the frame
for v in @vertices
disp = v.displacement
dispMag = v.displacement.magnitude()

# Move vertex
v.add(disp.divide(dispMag).multiply(Math.min(dispMag, temperature)))

if (v.x < 0)
v.x = 0
if (v.x > @width)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use else if since v.x cannot be < 0 and > width.

Alternatively, use the Point's max and min functions: v.max( 0, 0 ).min( @width, @height)

v.x = @width
if (v.y < 0)
v.y = 0
if (v.y > @height)
v.y = @height

# Cooling
temperature = @_cool(lambda, temperature)

# ## Move every vertex in the graph to a random position (within the frame)
# @return this ForceDirectedGraph object
randomize: () ->
for vertex in @vertices
x = Math.random() * @width
y = Math.random() * @height
vertex.moveTo(x, y);
return @


this.Edge = Edge;
this.Vertex = Vertex;
this.ForceDirectedGraph = ForceDirectedGraph;