cytoscape-spread
Version:
The Spread physics simulation layout for Cytoscape.js
781 lines (674 loc) • 20 kB
JavaScript
window.foograph = {
/**
* Insert a vertex into this graph.
*
* @param vertex A valid Vertex instance
*/
insertVertex: function(vertex) {
this.vertices.push(vertex);
this.vertexCount++;
},
/**
* Insert an edge vertex1 --> vertex2.
*
* @param label Label for this edge
* @param weight Weight of this edge
* @param vertex1 Starting Vertex instance
* @param vertex2 Ending Vertex instance
* @return Newly created Edge instance
*/
insertEdge: function(label, weight, vertex1, vertex2, style) {
var e1 = new foograph.Edge(label, weight, vertex2, style);
var e2 = new foograph.Edge(null, weight, vertex1, null);
vertex1.edges.push(e1);
vertex2.reverseEdges.push(e2);
return e1;
},
/**
* Delete edge.
*
* @param vertex Starting vertex
* @param edge Edge to remove
*/
removeEdge: function(vertex1, vertex2) {
for (var i = vertex1.edges.length - 1; i >= 0; i--) {
if (vertex1.edges[i].endVertex == vertex2) {
vertex1.edges.splice(i,1);
break;
}
}
for (var i = vertex2.reverseEdges.length - 1; i >= 0; i--) {
if (vertex2.reverseEdges[i].endVertex == vertex1) {
vertex2.reverseEdges.splice(i,1);
break;
}
}
},
/**
* Delete vertex.
*
* @param vertex Vertex to remove from the graph
*/
removeVertex: function(vertex) {
for (var i = vertex.edges.length - 1; i >= 0; i-- ) {
this.removeEdge(vertex, vertex.edges[i].endVertex);
}
for (var i = vertex.reverseEdges.length - 1; i >= 0; i-- ) {
this.removeEdge(vertex.reverseEdges[i].endVertex, vertex);
}
for (var i = this.vertices.length - 1; i >= 0; i-- ) {
if (this.vertices[i] == vertex) {
this.vertices.splice(i,1);
break;
}
}
this.vertexCount--;
},
/**
* Plots this graph to a canvas.
*
* @param canvas A proper canvas instance
*/
plot: function(canvas) {
var i = 0;
/* Draw edges first */
for (i = 0; i < this.vertices.length; i++) {
var v = this.vertices[i];
if (!v.hidden) {
for (var j = 0; j < v.edges.length; j++) {
var e = v.edges[j];
/* Draw edge (if not hidden) */
if (!e.hidden)
e.draw(canvas, v);
}
}
}
/* Draw the vertices. */
for (i = 0; i < this.vertices.length; i++) {
v = this.vertices[i];
/* Draw vertex (if not hidden) */
if (!v.hidden)
v.draw(canvas);
}
},
/**
* Graph object constructor.
*
* @param label Label of this graph
* @param directed true or false
*/
Graph: function (label, directed) {
/* Fields. */
this.label = label;
this.vertices = new Array();
this.directed = directed;
this.vertexCount = 0;
/* Graph methods. */
this.insertVertex = foograph.insertVertex;
this.removeVertex = foograph.removeVertex;
this.insertEdge = foograph.insertEdge;
this.removeEdge = foograph.removeEdge;
this.plot = foograph.plot;
},
/**
* Vertex object constructor.
*
* @param label Label of this vertex
* @param next Reference to the next vertex of this graph
* @param firstEdge First edge of a linked list of edges
*/
Vertex: function(label, x, y, style) {
this.label = label;
this.edges = new Array();
this.reverseEdges = new Array();
this.x = x;
this.y = y;
this.dx = 0;
this.dy = 0;
this.level = -1;
this.numberOfParents = 0;
this.hidden = false;
this.fixed = false; // Fixed vertices are static (unmovable)
if(style != null) {
this.style = style;
}
else { // Default
this.style = new foograph.VertexStyle('ellipse', 80, 40, '#ffffff', '#000000', true);
}
},
/**
* VertexStyle object type for defining vertex style options.
*
* @param shape Shape of the vertex ('ellipse' or 'rect')
* @param width Width in px
* @param height Height in px
* @param fillColor The color with which the vertex is drawn (RGB HEX string)
* @param borderColor The color with which the border of the vertex is drawn (RGB HEX string)
* @param showLabel Show the vertex label or not
*/
VertexStyle: function(shape, width, height, fillColor, borderColor, showLabel) {
this.shape = shape;
this.width = width;
this.height = height;
this.fillColor = fillColor;
this.borderColor = borderColor;
this.showLabel = showLabel;
},
/**
* Edge object constructor.
*
* @param label Label of this edge
* @param next Next edge reference
* @param weight Edge weight
* @param endVertex Destination Vertex instance
*/
Edge: function (label, weight, endVertex, style) {
this.label = label;
this.weight = weight;
this.endVertex = endVertex;
this.style = null;
this.hidden = false;
// Curving information
this.curved = false;
this.controlX = -1; // Control coordinates for Bezier curve drawing
this.controlY = -1;
this.original = null; // If this is a temporary edge it holds the original edge
if(style != null) {
this.style = style;
}
else { // Set to default
this.style = new foograph.EdgeStyle(2, '#000000', true, false);
}
},
/**
* EdgeStyle object type for defining vertex style options.
*
* @param width Edge line width
* @param color The color with which the edge is drawn
* @param showArrow Draw the edge arrow (only if directed)
* @param showLabel Show the edge label or not
*/
EdgeStyle: function(width, color, showArrow, showLabel) {
this.width = width;
this.color = color;
this.showArrow = showArrow;
this.showLabel = showLabel;
},
/**
* This file is part of foograph Javascript graph library.
*
* Description: Random vertex layout manager
*/
/**
* Class constructor.
*
* @param width Layout width
* @param height Layout height
*/
RandomVertexLayout: function (width, height) {
this.width = width;
this.height = height;
},
/**
* This file is part of foograph Javascript graph library.
*
* Description: Fruchterman-Reingold force-directed vertex
* layout manager
*/
/**
* Class constructor.
*
* @param width Layout width
* @param height Layout height
* @param iterations Number of iterations -
* with more iterations it is more likely the layout has converged into a static equilibrium.
*/
ForceDirectedVertexLayout: function (width, height, iterations, randomize, eps) {
this.width = width;
this.height = height;
this.iterations = iterations;
this.randomize = randomize;
this.eps = eps;
this.callback = function() {};
},
A: 1.5, // Fine tune attraction
R: 0.5 // Fine tune repulsion
};
/**
* toString overload for easier debugging
*/
foograph.Vertex.prototype.toString = function() {
return "[v:" + this.label + "] ";
};
/**
* toString overload for easier debugging
*/
foograph.Edge.prototype.toString = function() {
return "[e:" + this.endVertex.label + "] ";
};
/**
* Draw vertex method.
*
* @param canvas jsGraphics instance
*/
foograph.Vertex.prototype.draw = function(canvas) {
var x = this.x;
var y = this.y;
var width = this.style.width;
var height = this.style.height;
var shape = this.style.shape;
canvas.setStroke(2);
canvas.setColor(this.style.fillColor);
if(shape == 'rect') {
canvas.fillRect(x, y, width, height);
canvas.setColor(this.style.borderColor);
canvas.drawRect(x, y, width, height);
}
else { // Default to ellipse
canvas.fillEllipse(x, y, width, height);
canvas.setColor(this.style.borderColor);
canvas.drawEllipse(x, y, width, height);
}
if(this.style.showLabel) {
canvas.drawStringRect(this.label, x, y + height/2 - 7, width, 'center');
}
};
/**
* Fits the graph into the bounding box
*
* @param width
* @param height
* @param preserveAspect
*/
foograph.Graph.prototype.normalize = function(width, height, preserveAspect) {
for (var i8 in this.vertices) {
var v = this.vertices[i8];
v.oldX = v.x;
v.oldY = v.y;
}
var mnx = width * 0.1;
var mxx = width * 0.9;
var mny = height * 0.1;
var mxy = height * 0.9;
if (preserveAspect == null)
preserveAspect = true;
var minx = Number.MAX_VALUE;
var miny = Number.MAX_VALUE;
var maxx = Number.MIN_VALUE;
var maxy = Number.MIN_VALUE;
for (var i7 in this.vertices) {
var v = this.vertices[i7];
if (v.x < minx) minx = v.x;
if (v.y < miny) miny = v.y;
if (v.x > maxx) maxx = v.x;
if (v.y > maxy) maxy = v.y;
}
var kx = (mxx-mnx) / (maxx - minx);
var ky = (mxy-mny) / (maxy - miny);
if (preserveAspect) {
kx = Math.min(kx, ky);
ky = Math.min(kx, ky);
}
var newMaxx = Number.MIN_VALUE;
var newMaxy = Number.MIN_VALUE;
for (var i8 in this.vertices) {
var v = this.vertices[i8];
v.x = (v.x - minx) * kx;
v.y = (v.y - miny) * ky;
if (v.x > newMaxx) newMaxx = v.x;
if (v.y > newMaxy) newMaxy = v.y;
}
var dx = ( width - newMaxx ) / 2.0;
var dy = ( height - newMaxy ) / 2.0;
for (var i8 in this.vertices) {
var v = this.vertices[i8];
v.x += dx;
v.y += dy;
}
};
/**
* Draw edge method. Draws edge "v" --> "this".
*
* @param canvas jsGraphics instance
* @param v Start vertex
*/
foograph.Edge.prototype.draw = function(canvas, v) {
var x1 = Math.round(v.x + v.style.width/2);
var y1 = Math.round(v.y + v.style.height/2);
var x2 = Math.round(this.endVertex.x + this.endVertex.style.width/2);
var y2 = Math.round(this.endVertex.y + this.endVertex.style.height/2);
// Control point (needed only for curved edges)
var x3 = this.controlX;
var y3 = this.controlY;
// Arrow tip and angle
var X_TIP, Y_TIP, ANGLE;
/* Quadric Bezier curve definition. */
function Bx(t) { return (1-t)*(1-t)*x1 + 2*(1-t)*t*x3 + t*t*x2; }
function By(t) { return (1-t)*(1-t)*y1 + 2*(1-t)*t*y3 + t*t*y2; }
canvas.setStroke(this.style.width);
canvas.setColor(this.style.color);
if(this.curved) { // Draw a quadric Bezier curve
this.curved = false; // Reset
var t = 0, dt = 1/10;
var xs = x1, ys = y1, xn, yn;
while (t < 1-dt) {
t += dt;
xn = Bx(t);
yn = By(t);
canvas.drawLine(xs, ys, xn, yn);
xs = xn;
ys = yn;
}
// Set the arrow tip coordinates
X_TIP = xs;
Y_TIP = ys;
// Move the tip to (0,0) and calculate the angle
// of the arrow head
ANGLE = angularCoord(Bx(1-2*dt) - X_TIP, By(1-2*dt) - Y_TIP);
} else {
canvas.drawLine(x1, y1, x2, y2);
// Set the arrow tip coordinates
X_TIP = x2;
Y_TIP = y2;
// Move the tip to (0,0) and calculate the angle
// of the arrow head
ANGLE = angularCoord(x1 - X_TIP, y1 - Y_TIP);
}
if(this.style.showArrow) {
drawArrow(ANGLE, X_TIP, Y_TIP);
}
// TODO
if(this.style.showLabel) {
}
/**
* Draws an edge arrow.
* @param phi The angle (in radians) of the arrow in polar coordinates.
* @param x X coordinate of the arrow tip.
* @param y Y coordinate of the arrow tip.
*/
function drawArrow(phi, x, y)
{
// Arrow bounding box (in px)
var H = 50;
var W = 10;
// Set cartesian coordinates of the arrow
var p11 = 0, p12 = 0;
var p21 = H, p22 = W/2;
var p31 = H, p32 = -W/2;
// Convert to polar coordinates
var r2 = radialCoord(p21, p22);
var r3 = radialCoord(p31, p32);
var phi2 = angularCoord(p21, p22);
var phi3 = angularCoord(p31, p32);
// Rotate the arrow
phi2 += phi;
phi3 += phi;
// Update cartesian coordinates
p21 = r2 * Math.cos(phi2);
p22 = r2 * Math.sin(phi2);
p31 = r3 * Math.cos(phi3);
p32 = r3 * Math.sin(phi3);
// Translate
p11 += x;
p12 += y;
p21 += x;
p22 += y;
p31 += x;
p32 += y;
// Draw
canvas.fillPolygon(new Array(p11, p21, p31), new Array(p12, p22, p32));
}
/**
* Get the angular coordinate.
* @param x X coordinate
* @param y Y coordinate
*/
function angularCoord(x, y)
{
var phi = 0.0;
if (x > 0 && y >= 0) {
phi = Math.atan(y/x);
}
if (x > 0 && y < 0) {
phi = Math.atan(y/x) + 2*Math.PI;
}
if (x < 0) {
phi = Math.atan(y/x) + Math.PI;
}
if (x = 0 && y > 0) {
phi = Math.PI/2;
}
if (x = 0 && y < 0) {
phi = 3*Math.PI/2;
}
return phi;
}
/**
* Get the radian coordiante.
* @param x1
* @param y1
* @param x2
* @param y2
*/
function radialCoord(x, y)
{
return Math.sqrt(x*x + y*y);
}
};
/**
* Calculates the coordinates based on pure chance.
*
* @param graph A valid graph instance
*/
foograph.RandomVertexLayout.prototype.layout = function(graph) {
for (var i = 0; i<graph.vertices.length; i++) {
var v = graph.vertices[i];
v.x = Math.round(Math.random() * this.width);
v.y = Math.round(Math.random() * this.height);
}
};
/**
* Identifies connected components of a graph and creates "central"
* vertices for each component. If there is more than one component,
* all central vertices of individual components are connected to
* each other to prevent component drift.
*
* @param graph A valid graph instance
* @return A list of component center vertices or null when there
* is only one component.
*/
foograph.ForceDirectedVertexLayout.prototype.__identifyComponents = function(graph) {
var componentCenters = new Array();
var components = new Array();
// Depth first search
function dfs(vertex)
{
var stack = new Array();
var component = new Array();
var centerVertex = new foograph.Vertex("component_center", -1, -1);
centerVertex.hidden = true;
componentCenters.push(centerVertex);
components.push(component);
function visitVertex(v)
{
component.push(v);
v.__dfsVisited = true;
for (var i in v.edges) {
var e = v.edges[i];
if (!e.hidden)
stack.push(e.endVertex);
}
for (var i in v.reverseEdges) {
if (!v.reverseEdges[i].hidden)
stack.push(v.reverseEdges[i].endVertex);
}
}
visitVertex(vertex);
while (stack.length > 0) {
var u = stack.pop();
if (!u.__dfsVisited && !u.hidden) {
visitVertex(u);
}
}
}
// Clear DFS visited flag
for (var i in graph.vertices) {
var v = graph.vertices[i];
v.__dfsVisited = false;
}
// Iterate through all vertices starting DFS from each vertex
// that hasn't been visited yet.
for (var k in graph.vertices) {
var v = graph.vertices[k];
if (!v.__dfsVisited && !v.hidden)
dfs(v);
}
// Interconnect all center vertices
if (componentCenters.length > 1) {
for (var i in componentCenters) {
graph.insertVertex(componentCenters[i]);
}
for (var i in components) {
for (var j in components[i]) {
// Connect visited vertex to "central" component vertex
edge = graph.insertEdge("", 1, components[i][j], componentCenters[i]);
edge.hidden = true;
}
}
for (var i in componentCenters) {
for (var j in componentCenters) {
if (i != j) {
e = graph.insertEdge("", 3, componentCenters[i], componentCenters[j]);
e.hidden = true;
}
}
}
return componentCenters;
}
return null;
};
/**
* Calculates the coordinates based on force-directed placement
* algorithm.
*
* @param graph A valid graph instance
*/
foograph.ForceDirectedVertexLayout.prototype.layout = function(graph) {
this.graph = graph;
var area = this.width * this.height;
var k = Math.sqrt(area / graph.vertexCount);
var t = this.width / 10; // Temperature.
var dt = t / (this.iterations + 1);
var eps = this.eps; // Minimum distance between the vertices
// Attractive and repulsive forces
function Fa(z) { return foograph.A*z*z/k; }
function Fr(z) { return foograph.R*k*k/z; }
function Fw(z) { return 1/z*z; } // Force emited by the walls
// Initiate component identification and virtual vertex creation
// to prevent disconnected graph components from drifting too far apart
centers = this.__identifyComponents(graph);
// Assign initial random positions
if(this.randomize) {
randomLayout = new foograph.RandomVertexLayout(this.width, this.height);
randomLayout.layout(graph);
}
// Run through some iterations
for (var q = 0; q < this.iterations; q++) {
/* Calculate repulsive forces. */
for (var i1 in graph.vertices) {
var v = graph.vertices[i1];
v.dx = 0;
v.dy = 0;
// Do not move fixed vertices
if(!v.fixed) {
for (var i2 in graph.vertices) {
var u = graph.vertices[i2];
if (v != u && !u.fixed) {
/* Difference vector between the two vertices. */
var difx = v.x - u.x;
var dify = v.y - u.y;
/* Length of the dif vector. */
var d = Math.max(eps, Math.sqrt(difx*difx + dify*dify));
var force = Fr(d);
v.dx = v.dx + (difx/d) * force;
v.dy = v.dy + (dify/d) * force;
}
}
/* Treat the walls as static objects emiting force Fw. */
// Calculate the sum of "wall" forces in (v.x, v.y)
/*
var x = Math.max(eps, v.x);
var y = Math.max(eps, v.y);
var wx = Math.max(eps, this.width - v.x);
var wy = Math.max(eps, this.height - v.y); // Gotta love all those NaN's :)
var Rx = Fw(x) - Fw(wx);
var Ry = Fw(y) - Fw(wy);
v.dx = v.dx + Rx;
v.dy = v.dy + Ry;
*/
}
}
/* Calculate attractive forces. */
for (var i3 in graph.vertices) {
var v = graph.vertices[i3];
// Do not move fixed vertices
if(!v.fixed) {
for (var i4 in v.edges) {
var e = v.edges[i4];
var u = e.endVertex;
var difx = v.x - u.x;
var dify = v.y - u.y;
var d = Math.max(eps, Math.sqrt(difx*difx + dify*dify));
var force = Fa(d);
/* Length of the dif vector. */
var d = Math.max(eps, Math.sqrt(difx*difx + dify*dify));
v.dx = v.dx - (difx/d) * force;
v.dy = v.dy - (dify/d) * force;
u.dx = u.dx + (difx/d) * force;
u.dy = u.dy + (dify/d) * force;
}
}
}
/* Limit the maximum displacement to the temperature t
and prevent from being displaced outside frame. */
for (var i5 in graph.vertices) {
var v = graph.vertices[i5];
if(!v.fixed) {
/* Length of the displacement vector. */
var d = Math.max(eps, Math.sqrt(v.dx*v.dx + v.dy*v.dy));
/* Limit to the temperature t. */
v.x = v.x + (v.dx/d) * Math.min(d, t);
v.y = v.y + (v.dy/d) * Math.min(d, t);
/* Stay inside the frame. */
/*
borderWidth = this.width / 50;
if (v.x < borderWidth) {
v.x = borderWidth;
} else if (v.x > this.width - borderWidth) {
v.x = this.width - borderWidth;
}
if (v.y < borderWidth) {
v.y = borderWidth;
} else if (v.y > this.height - borderWidth) {
v.y = this.height - borderWidth;
}
*/
v.x = Math.round(v.x);
v.y = Math.round(v.y);
}
}
/* Cool. */
t -= dt;
if (q % 10 == 0) {
this.callback();
}
}
// Remove virtual center vertices
if (centers) {
for (var i in centers) {
graph.removeVertex(centers[i]);
}
}
graph.normalize(this.width, this.height, true);
};
module.exports = foograph;