### * Dracula Graph Layout and Drawing Framework 0.0.3alpha * (c) 2010 Philipp Strathausen , http://strathausen.eu * * Contributions by: * Branched by Jake Stothard . * * based on the Graph JavaScript framework, version 0.0.1 * (c) 2006 Aslak Hellesoy * (c) 2006 Dave Hoover * * Ported from Graph::Layouter::Spring in * http://search.cpan.org/~pasky/Graph-Layderer-0.02/ * The algorithm is based on a spring-style layouter of a Java-based social * network tracker PieSpy written by Paul Mutton Epaul@jibble.orgE. * * This code is freely distributable under the terms of an MIT-style license. * For details, see the Graph web site: http://dev.buildpatternd.com/trac * * Links: * * Graph Dracula JavaScript Framework: * http://graphdracula.net * * Demo of the original applet: * http://redsquirrel.com/dave/work/webdep/ * * Mirrored original source code at snipplr: * http://snipplr.com/view/1950/graph-javascript-framework-version-001/ * * Original usage example: * http://ajaxian.com/archives/new-javascriptcanvas-graph-library * ### ### Edge Factory ### AbstractEdge = -> AbstractEdge.prototype = hide: -> @connection.fg.hide() @connection.bg && @bg.connection.hide() EdgeFactory = -> @template = new AbstractEdge() @template.style = new Object() @template.style.directed = false @template.weight = 1 EdgeFactory.prototype = build: (source, target) -> e = jQuery.extend true, {}, @template e.source = source e.target = target e ### Graph ### Graph = -> @nodes = {} @edges = [] @snapshots = [] # previous graph states TODO to be implemented @edgeFactory = new EdgeFactory() Graph.prototype = ### add a node @id the node's ID (string or number) @content (optional, dictionary) can contain any information that is being interpreted by the layout algorithm or the graph representation ### addNode: (id, content) -> # testing if node is already existing in the graph if @nodes[id] == undefined @nodes[id] = new Graph.Node id, content @nodes[id] addEdge: (source, target, style) -> s = @addNode source t = @addNode target var edge = @edgeFactory.build s, t jQuery.extend edge.style, style s.edges.push edge @edges.push edge # NOTE: Even directed edges are added to both nodes. t.edges.push edge # TODO to be implemented # Preserve a copy of the graph state (nodes, positions, ...) # @comment a comment describing the state snapShot: (comment) -> ###/* FIXME var graph = new Graph() graph.nodes = jQuery.extend(true, {}, @nodes) graph.edges = jQuery.extend(true, {}, @edges) @snapshots.push({comment: comment, graph: graph}) */ ### removeNode: (id) -> delete @nodes[id] for i = 0; i < @edges.length; i++ if @edges[i].source.id == id || @edges[i].target.id == id @edges.splice(i, 1) i-- /* * Node */ Graph.Node = (id, node) -> node = node || {} node.id = id node.edges = [] node.hide = -> @hidden = true @shape && @shape.hide() # FIXME this is representation specific code and should be elsewhere */ for(i in @edges) (@edges[i].source.id == id || @edges[i].target == id) && @edges[i].hide && @edges[i].hide() node.show = -> @hidden = false @shape && @shape.show() for(i in @edges) (@edges[i].source.id == id || @edges[i].target == id) && @edges[i].show && @edges[i].show() node Graph.Node.prototype = { } ### Renderer base class ### Graph.Renderer = { } ### Renderer implementation using RaphaelJS ### Graph.Renderer.Raphael = (element, graph, width, height) -> @width = width || 400 @height = height || 400 var selfRef = this @r = Raphael element, @width, @height @radius = 40 # max dimension of a node @graph = graph @mouse_in = false # TODO default node rendering if(!@graph.render) { @graph.render = -> return } } /* * Dragging */ @isDrag = false @dragger = (e) -> @dx = e.clientX @dy = e.clientY selfRef.isDrag = this @set && @set.animate "fill-opacity": .1, 200 && @set.toFront() e.preventDefault && e.preventDefault() document.onmousemove = (e) { e = e || window.event if (selfRef.isDrag) { var bBox = selfRef.isDrag.set.getBBox() // TODO round the coordinates here (eg. for proper image representation) var newX = e.clientX - selfRef.isDrag.dx + (bBox.x + bBox.width / 2) var newY = e.clientY - selfRef.isDrag.dy + (bBox.y + bBox.height / 2) /* prevent shapes from being dragged out of the canvas */ var clientX = e.clientX - (newX < 20 ? newX - 20 : newX > selfRef.width - 20 ? newX - selfRef.width + 20 : 0) var clientY = e.clientY - (newY < 20 ? newY - 20 : newY > selfRef.height - 20 ? newY - selfRef.height + 20 : 0) selfRef.isDrag.set.translate(clientX - Math.round(selfRef.isDrag.dx), clientY - Math.round(selfRef.isDrag.dy)) // console.log(clientX - Math.round(selfRef.isDrag.dx), clientY - Math.round(selfRef.isDrag.dy)) for (var i in selfRef.graph.edges) { selfRef.graph.edges[i].connection && selfRef.graph.edges[i].connection.draw() } //selfRef.r.safari() selfRef.isDrag.dx = clientX selfRef.isDrag.dy = clientY } } document.onmouseup = -> selfRef.isDrag && selfRef.isDrag.set.animate({"fill-opacity": .6}, 500) selfRef.isDrag = false } @draw() } Graph.Renderer.Raphael.prototype = { translate: (point) { return [ (point[0] - @graph.layoutMinX) * @factorX + @radius, (point[1] - @graph.layoutMinY) * @factorY + @radius ] }, rotate: (point, length, angle) { var dx = length * Math.cos(angle) var dy = length * Math.sin(angle) return [point[0]+dx, point[1]+dy] }, draw: -> @factorX = (@width - 2 * @radius) / (@graph.layoutMaxX - @graph.layoutMinX) @factorY = (@height - 2 * @radius) / (@graph.layoutMaxY - @graph.layoutMinY) for (i in @graph.nodes) { @drawNode(@graph.nodes[i]) } for (var i = 0; i < @graph.edges.length; i++) { @drawEdge(@graph.edges[i]) } }, drawNode: (node) { var point = @translate([node.layoutPosX, node.layoutPosY]) node.point = point /* if node has already been drawn, move the nodes */ if(node.shape) { var oBBox = node.shape.getBBox() var opoint = { x: oBBox.x + oBBox.width / 2, y: oBBox.y + oBBox.height / 2} node.shape.translate(Math.round(point[0] - opoint.x), Math.round(point[1] - opoint.y)) @r.safari() return node }/* else, draw new nodes */ var shape /* if a node renderer is provided by the user, then use it or the default render instead */ if(!node.render) { node.render = (r, node) { /* the default node drawing */ var color = Raphael.getColor() var ellipse = r.ellipse(0, 0, 30, 20).attr({fill: color, stroke: color, "stroke-width": 2}) /* set DOM node ID */ ellipse.node.id = node.label || node.id shape = r.set(). push(ellipse). push(r.text(0, 30, node.label || node.id)) return shape } } /* or check for an ajax representation of the nodes */ if(node.shapes) { // TODO ajax representation evaluation } shape = node.render(@r, node).hide() shape.attr({"fill-opacity": .6}) /* re-reference to the node an element belongs to, needed for dragging all elements of a node */ shape.items.forEach((item){ item.set = shape; item.node.style.cursor = "move"; }) shape.mousedown(@dragger) var box = shape.getBBox() shape.translate(Math.round(point[0]-(box.x+box.width/2)),Math.round(point[1]-(box.y+box.height/2))) //console.log(box,point) node.hidden || shape.show() node.shape = shape }, drawEdge: (edge) { /* if this edge already exists the other way around and is undirected */ if(edge.backedge) return if(edge.source.hidden || edge.target.hidden) { edge.connection && edge.connection.fg.hide() | edge.connection.bg && edge.connection.bg.hide() return } /* if edge already has been drawn, only refresh the edge */ if(!edge.connection) { edge.style && edge.style.callback && edge.style.callback(edge); // TODO move this somewhere else edge.connection = @r.connection(edge.source.shape, edge.target.shape, edge.style) return } //FIXME showing doesn't work well edge.connection.fg.show() edge.connection.bg && edge.connection.bg.show() edge.connection.draw() } } Graph.Layout = {} Graph.Layout.Spring = (graph) { @graph = graph @iterations = 500 @maxRepulsiveForceDistance = 6 @k = 2 @c = 0.01 @maxVertexMovement = 0.5 @layout() } Graph.Layout.Spring.prototype = { layout: -> @layoutPrepare() for (var i = 0; i < @iterations; i++) { @layoutIteration() } @layoutCalcBounds() }, layoutPrepare: -> for (i in @graph.nodes) { var node = @graph.nodes[i] node.layoutPosX = 0 node.layoutPosY = 0 node.layoutForceX = 0 node.layoutForceY = 0 } }, layoutCalcBounds: -> var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity for (i in @graph.nodes) { var x = @graph.nodes[i].layoutPosX var y = @graph.nodes[i].layoutPosY if(x > maxx) maxx = x if(x < minx) minx = x if(y > maxy) maxy = y if(y < miny) miny = y } @graph.layoutMinX = minx @graph.layoutMaxX = maxx @graph.layoutMinY = miny @graph.layoutMaxY = maxy }, layoutIteration: -> // Forces on nodes due to node-node repulsions var prev = new Array() for(var c in @graph.nodes) { var node1 = @graph.nodes[c] for (var d in prev) { var node2 = @graph.nodes[prev[d]] @layoutRepulsive(node1, node2) } prev.push(c) } // Forces on nodes due to edge attractions for (var i = 0; i < @graph.edges.length; i++) { var edge = @graph.edges[i] @layoutAttractive(edge); } // Move by the given force for (i in @graph.nodes) { var node = @graph.nodes[i] var xmove = @c * node.layoutForceX var ymove = @c * node.layoutForceY var max = @maxVertexMovement if(xmove > max) xmove = max if(xmove < -max) xmove = -max if(ymove > max) ymove = max if(ymove < -max) ymove = -max node.layoutPosX += xmove node.layoutPosY += ymove node.layoutForceX = 0 node.layoutForceY = 0 } }, layoutRepulsive: (node1, node2) { var dx = node2.layoutPosX - node1.layoutPosX var dy = node2.layoutPosY - node1.layoutPosY var d2 = dx * dx + dy * dy if(d2 < 0.01) { dx = 0.1 * Math.random() + 0.1 dy = 0.1 * Math.random() + 0.1 var d2 = dx * dx + dy * dy } var d = Math.sqrt(d2) if(d < @maxRepulsiveForceDistance) { var repulsiveForce = @k * @k / d node2.layoutForceX += repulsiveForce * dx / d node2.layoutForceY += repulsiveForce * dy / d node1.layoutForceX -= repulsiveForce * dx / d node1.layoutForceY -= repulsiveForce * dy / d } }, layoutAttractive: (edge) { var node1 = edge.source var node2 = edge.target var dx = node2.layoutPosX - node1.layoutPosX var dy = node2.layoutPosY - node1.layoutPosY var d2 = dx * dx + dy * dy if(d2 < 0.01) { dx = 0.1 * Math.random() + 0.1 dy = 0.1 * Math.random() + 0.1 var d2 = dx * dx + dy * dy } var d = Math.sqrt(d2) if(d > @maxRepulsiveForceDistance) { d = @maxRepulsiveForceDistance d2 = d * d } var attractiveForce = (d2 - @k * @k) / @k if(edge.attraction == undefined) edge.attraction = 1 attractiveForce *= Math.log(edge.attraction) * 0.5 + 1 node2.layoutForceX -= attractiveForce * dx / d node2.layoutForceY -= attractiveForce * dy / d node1.layoutForceX += attractiveForce * dx / d node1.layoutForceY += attractiveForce * dy / d } } Graph.Layout.Ordered = (graph, order) { @graph = graph @order = order @layout() } Graph.Layout.Ordered.prototype = { layout: -> @layoutPrepare() @layoutCalcBounds() }, layoutPrepare: (order) { for (i in @graph.nodes) { var node = @graph.nodes[i] node.layoutPosX = 0 node.layoutPosY = 0 } var counter = 0 for (i in @order) { var node = @order[i] node.layoutPosX = counter node.layoutPosY = Math.random() counter++ } }, layoutCalcBounds: -> var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity for (i in @graph.nodes) { var x = @graph.nodes[i].layoutPosX var y = @graph.nodes[i].layoutPosY if(x > maxx) maxx = x if(x < minx) minx = x if(y > maxy) maxy = y if(y < miny) miny = y } @graph.layoutMinX = minx @graph.layoutMaxX = maxx @graph.layoutMinY = miny @graph.layoutMaxY = maxy } } /* * usefull JavaScript extensions, */ log(a) {console.log&&console.log(a);} /* * Raphael Tooltip Plugin * - attaches an element as a tooltip to another element * * Usage example, adding a rectangle as a tooltip to a circle: * * paper.circle(100,100,10).tooltip(paper.rect(0,0,20,30)) * * If you want to use more shapes, you'll have to put them into a set. * */ Raphael.el.tooltip = (tp) { @tp = tp @tp.o = {x: 0, y: 0} @tp.hide() @hover( (event){ @mousemove((event){ @tp.translate(event.clientX - @tp.o.x,event.clientY - @tp.o.y) @tp.o = {x: event.clientX, y: event.clientY} }) @tp.show().toFront() }, (event){ @tp.hide() @unmousemove() }) return this } /* For IE */ if (!Array.prototype.forEach) { Array.prototype.forEach = (fun /*, thisp*/) { var len = @length if (typeof fun != "") throw new TypeError() var thisp = arguments[1] for (var i = 0; i < len; i++) { if (i in this) fun.call(thisp, this[i], i, this) } } }