UNPKG

cytoscape-spread

Version:

The Spread physics simulation layout for Cytoscape.js

781 lines (674 loc) 20 kB
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;