diff --git a/index_example.html b/index_example.html index 1de5cd0..d877f8e 100644 --- a/index_example.html +++ b/index_example.html @@ -22,6 +22,7 @@ + diff --git a/layouts/force-directed-layout.js b/layouts/force-directed-layout.js index dd8b195..7c87e71 100644 --- a/layouts/force-directed-layout.js +++ b/layouts/force-directed-layout.js @@ -1,11 +1,13 @@ /** - @author David Piegza + @author David Piegza (@davidpiegza) + @author Timofey Rechkalov (@TRechkalov) Implements a force-directed layout, the algorithm is based on Fruchterman and Reingold and the JUNG implementation. - Needs the graph data structure Graph.js: + Needs the graph data structure Graph.js and the Vector3 object: https://github.com/davidpiegza/Graph-Visualization/blob/master/Graph.js + https://github.com/davidpiegza/Graph-Visualization/blob/master/utils/Vector3.js Parameters: graph - data structure @@ -87,77 +89,41 @@ Layout.ForceDirected = function(graph, options) { this.generate = function() { if(layout_iterations < this.max_iterations && temperature > 0.000001) { var start = new Date().getTime(); - var i; - - var delta_x, delta_y, delta_z, delta_length, delta_length_z, force, force_z; + var i, j, delta_length, force, change; // calculate repulsion for(i=0; i < nodes_length; i++) { var node_v = graph.nodes[i]; node_v.layout = node_v.layout || {}; if(i === 0) { - node_v.layout.offset_x = 0; - node_v.layout.offset_y = 0; - if(this.layout === "3d") { - node_v.layout.offset_z = 0; - } + node_v.layout.offset = new Vector3(); } node_v.layout.force = 0; - node_v.layout.tmp_pos_x = node_v.layout.tmp_pos_x || node_v.position.x; - node_v.layout.tmp_pos_y = node_v.layout.tmp_pos_y || node_v.position.y; - if(this.layout === "3d") { - node_v.layout.tmp_pos_z = node_v.layout.tmp_pos_z || node_v.position.z; - } + node_v.layout.tmp_pos = node_v.layout.tmp_pos || new Vector3().setVector(node_v.position); - for(var j=i+1; j < nodes_length; j++) { + for(j=i+1; j < nodes_length; j++) { var node_u = graph.nodes[j]; if(i != j) { node_u.layout = node_u.layout || {}; - node_u.layout.tmp_pos_x = node_u.layout.tmp_pos_x || node_u.position.x; - node_u.layout.tmp_pos_y = node_u.layout.tmp_pos_y || node_u.position.y; - if(this.layout === "3d") { - node_u.layout.tmp_pos_z = node_u.layout.tmp_pos_z || node_u.position.z; - } - delta_x = node_v.layout.tmp_pos_x - node_u.layout.tmp_pos_x; - delta_y = node_v.layout.tmp_pos_y - node_u.layout.tmp_pos_y; + node_u.layout.tmp_pos = node_u.layout.tmp_pos || new Vector3().setVector(node_u.position); - if(this.layout === "3d") { - delta_z = node_v.layout.tmp_pos_z - node_u.layout.tmp_pos_z; - } - - delta_length = Math.max(EPSILON, Math.sqrt((delta_x * delta_x) + (delta_y * delta_y))); - if(this.layout === "3d") { - delta_length_z = Math.max(EPSILON, Math.sqrt((delta_z * delta_z) + (delta_y * delta_y))); - } + delta = node_v.layout.tmp_pos.clone().sub(node_u.layout.tmp_pos); + delta_length = Math.max(EPSILON, Math.sqrt(delta.clone().multiply(delta).sum())); force = (repulsion_constant * repulsion_constant) / delta_length; - if(this.layout === "3d") { - force_z = (repulsion_constant * repulsion_constant) / delta_length_z; - } - node_v.layout.force += force; node_u.layout.force += force; - node_v.layout.offset_x += (delta_x / delta_length) * force; - node_v.layout.offset_y += (delta_y / delta_length) * force; - if(i === 0) { - node_u.layout.offset_x = 0; - node_u.layout.offset_y = 0; - if(this.layout === "3d") { - node_u.layout.offset_z = 0; - } + node_u.layout.offset = new Vector3(); } - node_u.layout.offset_x -= (delta_x / delta_length) * force; - node_u.layout.offset_y -= (delta_y / delta_length) * force; - if(this.layout === "3d") { - node_v.layout.offset_z += (delta_z / delta_length_z) * force_z; - node_u.layout.offset_z -= (delta_z / delta_length_z) * force_z; - } + change = delta.clone().multiply(new Vector3().setScalar(force/delta_length)); + node_v.layout.offset.add(change); + node_u.layout.offset.sub(change); } } } @@ -165,62 +131,40 @@ Layout.ForceDirected = function(graph, options) { // calculate attraction for(i=0; i < edges_length; i++) { var edge = graph.edges[i]; - delta_x = edge.source.layout.tmp_pos_x - edge.target.layout.tmp_pos_x; - delta_y = edge.source.layout.tmp_pos_y - edge.target.layout.tmp_pos_y; - if(this.layout === "3d") { - delta_z = edge.source.layout.tmp_pos_z - edge.target.layout.tmp_pos_z; - } - - delta_length = Math.max(EPSILON, Math.sqrt((delta_x * delta_x) + (delta_y * delta_y))); - if(this.layout === "3d") { - delta_length_z = Math.max(EPSILON, Math.sqrt((delta_z * delta_z) + (delta_y * delta_y))); - } + delta = edge.source.layout.tmp_pos.clone().sub(edge.target.layout.tmp_pos); + delta_length = Math.max(EPSILON, Math.sqrt(delta.clone().multiply(delta).sum())); force = (delta_length * delta_length) / attraction_constant; - if(this.layout === "3d") { - force_z = (delta_length_z * delta_length_z) / attraction_constant; - } edge.source.layout.force -= force; edge.target.layout.force += force; - edge.source.layout.offset_x -= (delta_x / delta_length) * force; - edge.source.layout.offset_y -= (delta_y / delta_length) * force; - if(this.layout === "3d") { - edge.source.layout.offset_z -= (delta_z / delta_length_z) * force_z; - } - - edge.target.layout.offset_x += (delta_x / delta_length) * force; - edge.target.layout.offset_y += (delta_y / delta_length) * force; - if(this.layout === "3d") { - edge.target.layout.offset_z += (delta_z / delta_length_z) * force_z; - } + change = delta.clone().multiply(new Vector3().setScalar(force/delta_length)); + edge.target.layout.offset.add(change); + edge.source.layout.offset.sub(change); } // calculate positions for(i=0; i < nodes_length; i++) { var node = graph.nodes[i]; - delta_length = Math.max(EPSILON, Math.sqrt(node.layout.offset_x * node.layout.offset_x + node.layout.offset_y * node.layout.offset_y)); - if(this.layout === "3d") { - delta_length_z = Math.max(EPSILON, Math.sqrt(node.layout.offset_z * node.layout.offset_z + node.layout.offset_y * node.layout.offset_y)); - } + delta_length = Math.max(EPSILON, Math.sqrt(node.layout.offset.clone().multiply(node.layout.offset).sum())); - node.layout.tmp_pos_x += (node.layout.offset_x / delta_length) * Math.min(delta_length, temperature); - node.layout.tmp_pos_y += (node.layout.offset_y / delta_length) * Math.min(delta_length, temperature); - if(this.layout === "3d") { - node.layout.tmp_pos_z += (node.layout.offset_z / delta_length_z) * Math.min(delta_length_z, temperature); - } + node.layout.tmp_pos.add(node.layout.offset.clone().multiply(new Vector3().setScalar(Math.min(delta_length, temperature) / delta_length))); var updated = true; - node.position.x -= (node.position.x-node.layout.tmp_pos_x)/10; - node.position.y -= (node.position.y-node.layout.tmp_pos_y)/10; - if(this.layout === "3d") { - node.position.z -= (node.position.z-node.layout.tmp_pos_z)/10; + var tmpPosition = new Vector3(node.position.x, node.position.y, node.position.z); + tmpPosition.sub(node.layout.tmp_pos).divide(new Vector3().setScalar(10)); + + node.position.x -= tmpPosition.x; + node.position.y -= tmpPosition.y; + + if(this.layout === '3d') { + node.position.z -= tmpPosition.z; } - // execute callback function if positions has been updated + // execute callback function if position has been updated if(updated && typeof callback_positionUpdated === 'function') { callback_positionUpdated(node); } diff --git a/utils/Vector3.js b/utils/Vector3.js new file mode 100644 index 0000000..15ebb21 --- /dev/null +++ b/utils/Vector3.js @@ -0,0 +1,227 @@ +/** + @author David Piegza + + Class representing a 3D vector. It is based on the three.js (https://threejs.org) Vector3 class. + */ + +function Vector3(x, y, z) { + this.x = x || 0; + this.y = y || 0; + this.z = z || 0; +} + +Object.assign(Vector3.prototype, { + set: function(x, y, z) { + this.x = x; + this.y = y; + this.z = z; + + return this; + }, + + setScalar: function(scalar) { + this.x = scalar; + this.y = scalar; + this.z = scalar; + + return this; + }, + + setVector: function(v) { + this.x = v.x; + this.y = v.y; + this.z = v.z; + + return this; + }, + + setX: function(x) { + this.x = x; + + return this; + }, + + setY: function(y) { + this.y = y; + + return this; + }, + + setZ: function(z) { + this.z = z; + + return this; + }, + + setComponent: function(index, value) { + switch (index) { + case 0: this.x = value; break; + case 1: this.y = value; break; + case 2: this.z = value; break; + default: throw new Error('index is out of range: ' + index); + } + + return this; + }, + + getComponent: function(index) { + switch(index) { + case 0: return this.x; + case 1: return this.y; + case 2: return this.z; + default: throw new Error('index is out of range: ' + index); + } + }, + + clone: function() { + return new this.constructor(this.x, this.y, this.z); + }, + + copy: function(v) { + this.x = v.x; + this.y = v.y; + this.z = v.z; + + return this; + }, + + add: function(v) { + this.x += v.x; + this.y += v.y; + this.z += v.z; + + return this; + }, + + addScalar: function(s) { + this.x += s; + this.y += s; + this.z += s; + + return this; + }, + + addVectors: function(a, b) { + this.x = a.x + b.x; + this.y = a.y + b.y; + this.z = a.z + b.z; + + return this; + }, + + addScaledVector: function(v, s) { + this.x += v.x * s; + this.y += v.y * s; + this.z += v.z * s; + + return this; + }, + + sub: function(v) { + this.x -= v.x; + this.y -= v.y; + this.z -= v.z; + + return this; + }, + + subScalar: function(s) { + this.x -= s; + this.y -= s; + this.z -= s; + + return this; + }, + + subVectors: function(a, b) { + this.x = a.x - b.x; + this.y = a.y - b.y; + this.z = a.z - b.z; + + return this; + }, + + multiply: function(v) { + this.x *= v.x; + this.y *= v.y; + this.z *= v.z; + + return this; + }, + + multiplyScalar: function(scalar) { + this.x *= scalar; + this.y *= scalar; + this.z *= scalar; + + return this; + }, + + multiplyVectors: function(a, b) { + this.x = a.x * b.x; + this.y = a.y * b.y; + this.z = a.z * b.z; + + return this; + }, + + divide: function(v) { + this.x /= v.x; + this.y /= v.y; + this.z /= v.z; + + return this; + }, + + divideScalar: function (scalar) { + return this.multiplyScalar(1 / scalar); + }, + + min: function(v) { + this.x = Math.min(this.x, v.x); + this.y = Math.min(this.y, v.y); + this.z = Math.min(this.z, v.z); + + return this; + }, + + max: function(v) { + this.x = Math.max(this.x, v.x); + this.y = Math.max(this.y, v.y); + this.z = Math.max(this.z, v.z); + + return this; + }, + + sum: function() { + return this.x + this.y + this.z; + }, + + floor: function() { + this.x = Math.floor(this.x); + this.y = Math.floor(this.y); + this.z = Math.floor(this.z); + + return this; + }, + + ceil: function() { + this.x = Math.ceil(this.x); + this.y = Math.ceil(this.y); + this.z = Math.ceil(this.z); + + return this; + }, + + round: function() { + this.x = Math.round(this.x); + this.y = Math.round(this.y); + this.z = Math.round(this.z); + + return this; + }, + + equals: function(v) { + return ((v.x === this.x) && (v.y === this.y) && (v.z === this.z)); + }, +});