Created
October 13, 2012 13:13
-
-
Save shinaisan/3884593 to your computer and use it in GitHub Desktop.
Sample of Javascript InfoVis Toolkit for drawing complete graphs with the ability to change shape of edges by dragging.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> | |
<title>Test</title> | |
<link rel="stylesheet" type="text/css" href="test.css"/> | |
<script type='text/javascript' src='jit.js'></script> | |
<script type='text/javascript' src='test.js'></script> | |
</head> | |
<body onload="init();"> | |
Find the original sample <a href="http://thejit.org/static/v20/Jit/Examples/ForceDirected/example1.html">here</a>. | |
<div id="infovis"></div> | |
<div> | |
<input type="button" value="Complete Graph" id="complete-graph-button"></input> | |
<input type="button" value="Compute" id="compute-button"></input> | |
<input type="button" value="Refresh" id="refresh-button"></input> | |
</div> | |
<div id="inner-details"></div> | |
<div id="log"></div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* -*- coding: utf-8-unix -*- */ | |
#infovis { | |
background-color:#1a1a1a; | |
width:800px; | |
height:800px; | |
margin:auto; | |
} | |
/*TOOLTIPS*/ | |
.tip { | |
color: #111; | |
width: 139px; | |
background-color: white; | |
border:1px solid #ccc; | |
opacity:0.8; | |
filter:alpha(opacity=80); | |
padding:17px; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* -*- coding: utf-8-unix -*- */ | |
/* Find the original sample here <http://thejit.org/static/v20/Jit/Examples/ForceDirected/example1.html>. */ | |
var labelType, useGradients, nativeTextSupport, animate; | |
(function() { | |
var ua = navigator.userAgent, | |
iStuff = ua.match(/iPhone/i) || ua.match(/iPad/i), | |
typeOfCanvas = typeof HTMLCanvasElement, | |
nativeCanvasSupport = (typeOfCanvas == 'object' || typeOfCanvas == 'function'), | |
textSupport = nativeCanvasSupport | |
&& (typeof document.createElement('canvas').getContext('2d').fillText == 'function'); | |
//I'm setting this based on the fact that ExCanvas provides text support for IE | |
//and that as of today iPhone/iPad current text support is lame | |
labelType = (!nativeCanvasSupport || (textSupport && !iStuff))? 'Native' : 'HTML'; | |
nativeTextSupport = labelType == 'Native'; | |
useGradients = nativeCanvasSupport; | |
animate = !(iStuff || !nativeCanvasSupport); | |
})(); | |
var Log = { | |
elem: false, | |
write: function(text){ | |
if (!this.elem) | |
this.elem = document.getElementById('log'); | |
this.elem.innerHTML = text; | |
} | |
}; | |
var genNodeName = (function() { | |
var count = 0; | |
return (function() { | |
count++; | |
return "NODE" + count; | |
}); | |
})(); | |
// (px1, py1): control point | |
function quadraticBezier(t, px0, py0, px1, py1, px2, py2) { | |
var a = (1 - t)*(1 - t); | |
var b = 2*t*(1 - t); | |
var c = t*t; | |
return { | |
x: a*px0 + b*px1 + c*px2, | |
y: a*py0 + b*py1 + c*py2 | |
}; | |
} | |
// (px1, py1): control point | |
function distToQuadraticBezier(x, y, px0, py0, px1, py1, px2, py2) { | |
var epsilon = 0.000001; | |
var ax = px1 - px0; | |
var ay = py1 - py0; | |
var bx = px0 - 2*px1 + px2; | |
var by = py0 - 2*py1 + py2; | |
// p0 relative to (x, y) | |
var rx = px0 - x; | |
var ry = py0 - y; | |
// coefficients | |
var k3 = bx*bx + by*by; | |
var k2 = 3*(ax*bx + ay*by); | |
var k1 = 2*(ax*ax + ay*ay) + rx*bx + ry*by; | |
var k0 = rx*ax + ry*ay; | |
// Solve cubic equation | |
var cubicSols = solveCubic(k3, k2, k1, k0); | |
var distance = function(x1, y1, x2, y2) { | |
var dx = x1 - x2; | |
var dy = y1 - y2; | |
return Math.sqrt(dx*dx + dy*dy); | |
}; | |
var t = 0; | |
var dist; | |
var tMin; | |
var distMin = Number.MAX_VALUE; | |
var d0 = distance(x, y, px0, py0); | |
var d2 = distance(x, y, px2, py2); | |
var posMin = { | |
x: 0, y: 0 | |
}; | |
if (cubicSols != null) { | |
for (var i = 0; i <= cubicSols.length; i++) { | |
t = cubicSols[i]; | |
if (t >= 0 && t <= 1) { | |
var pos = quadraticBezier(t, px0, py0, px1, py1, px2, py2); | |
dist = distance(x, y, pos.x, pos.y); | |
if (dist < distMin) { | |
tMin = t; | |
distMin = dist; | |
posMin.x = pos.x; | |
posMin.y = pos.y; | |
} | |
} | |
} | |
if (tMin != null && distMin < d0 && distMin < d2) { | |
return { | |
t: tMin, | |
pos: posMin, | |
dist: distMin | |
}; | |
} | |
} | |
if (d0 < d2) { | |
return { | |
t: 0, | |
pos: { | |
x: px0, | |
y: py0 | |
}, | |
dist: d0 | |
}; | |
} else { | |
return { | |
t: 1, | |
pos: { | |
x: px2, | |
y: py2 | |
}, | |
dist: d2 | |
}; | |
} | |
} | |
// Solve k3x^3 + k2x^2 + k1x + k0 = 0 | |
function solveCubic(k3, k2, k1, k0) { | |
var epsilon = 0.000001; | |
var sols = new Array(); | |
if (Math.abs(k3) > epsilon) { | |
var a = k2 / k3; | |
var b = k1 / k3; | |
var c = k0 / k3; | |
var p = b - a*a/3; | |
var q = a*(2*a*a - 9*b) / 27 + c; | |
var pcubed = p*p*p; | |
var D = q*q + 4*pcubed/27; | |
var offset = -a/3; | |
if (D > epsilon) { | |
var sqrtD = Math.sqrt(D); | |
var u = (-q + sqrtD) / 2; | |
var v = (-q - sqrtD) / 2; | |
u = (u >= 0) ? Math.pow(u, 1/3) : -Math.pow(-u, 1/3); | |
v = (v >= 0) ? Math.pow(v, 1/3) : -Math.pow(-v, 1/3); | |
sols[0] = u + v + offset; | |
return sols; | |
} else if (D < -epsilon) { | |
var u = 2*Math.sqrt(-p/3); | |
var v = Math.acos(-Math.sqrt(-27/pcubed)*q/2)/3; | |
sols[0] = u*Math.cos(v) + offset; | |
sols[1] = u*Math.cos(v + 2*Math.PI/3) + offset; | |
sols[2] = u*Math.cos(v + 4*Math.PI/3) + offset; | |
return sols; | |
} else { | |
var u = (q < 0) ? Math.pow(-q/2, 1/3) : -Math.pow(q/2, 1/3); | |
sols[0] = 2*u + offset; | |
sols[1] = -u + offset; | |
return sols; | |
} | |
} else { | |
var a = k2; | |
var b = k1; | |
var c = k0; | |
if (Math.abs(a) <= epsilon) { | |
if (Math.abs(b) <= epsilon) { | |
return null; | |
} else { | |
sols[0] = -c/b; | |
return sols; | |
} | |
} | |
var D = b*b - 4*a*c; | |
if (D <= -epsilon) { | |
return null; | |
} | |
if (D > epsilon) { | |
sqrtD = Math.sqrt(D); | |
sols[0] = (-b - D)/(2*a); | |
sols[1] = (-b + D)/(2*a); | |
return sols; | |
} else if (D < -epsilon) { | |
return null; | |
} else { | |
sols[0] = -b/(2*a); | |
return sols; | |
} | |
} | |
} | |
function init(){ | |
// init data | |
var json = [ | |
{ | |
id: "NODE0", | |
name: "NODE0" | |
} | |
]; | |
$jit.ForceDirected.Plot.EdgeTypes.implement( | |
{ | |
'quadratic': { | |
'render': function(adj, canvas) { | |
var from = adj.nodeFrom.pos.getc(true), | |
to = adj.nodeTo.pos.getc(true); | |
// this.edgeHelper.line.render(from, to, canvas); | |
var cp = adj.getData("controlPoint"); | |
if (!cp || fd.animating) { | |
cp = { | |
x: (from.x + to.x) / 2, | |
y: (from.y + to.y) / 2 | |
}; | |
adj.setData("controlPoint", cp); | |
} | |
var ctx = canvas.getCtx(); | |
ctx.beginPath(); | |
ctx.moveTo(from.x, from.y); | |
ctx.quadraticCurveTo(cp.x, cp.y, to.x, to.y); | |
ctx.stroke(); | |
/* Show control points. | |
ctx.beginPath(); | |
ctx.arc(cp.x, cp.y, this.edge.epsilon, 0, Math.PI * 2, true); | |
ctx.closePath(); | |
ctx.stroke(); | |
*/ | |
}, | |
'contains': function(adj, pos) { | |
// var cp = adj.getData("controlPoint"); | |
// return this.nodeHelper.circle.contains(pos, cp, this.edge.epsilon); | |
var p0 = adj.nodeFrom.pos.getc(true); | |
var p1 = adj.getData("controlPoint"); | |
var p2 = adj.nodeTo.pos.getc(true); | |
var dist = distToQuadraticBezier(pos.x, pos.y, | |
p0.x, p0.y, p1.x, p1.y, p2.x, p2.y); | |
return dist.dist < this.edge.epsilon; | |
} | |
} | |
}); | |
// init ForceDirected | |
var fd = new $jit.ForceDirected( | |
{ | |
//id of the visualization container | |
injectInto: 'infovis', | |
//Enable zooming and panning | |
//by scrolling and DnD | |
Navigation: { | |
enable: false, | |
//Enable panning events only if we're dragging the empty | |
//canvas (and not a node). | |
panning: 'avoid nodes', | |
zooming: 10 //zoom speed. higher is more sensible | |
}, | |
// Change node and edge styles such as | |
// color and width. | |
// These properties are also set per node | |
// with dollar prefixed data-properties in the | |
// JSON structure. | |
Node: { | |
overridable: true, | |
color: "#ff8800", | |
dim: 10 | |
}, | |
Edge: { | |
type: 'quadratic', | |
epsilon: 4, | |
overridable: true, | |
color: '#23A4FF', | |
lineWidth: 1.4 | |
}, | |
//Native canvas text styling | |
Label: { | |
type: labelType, //Native or HTML | |
size: 10, | |
style: 'bold' | |
}, | |
//Add Tips | |
Tips: { | |
enable: true, | |
onShow: function(tip, node) { | |
//count connections | |
var count = 0; | |
node.eachAdjacency(function() { count++; }); | |
//display node info in tooltip | |
tip.innerHTML = "<div class=\"tip-title\">" + node.name + "</div>" | |
+ "<div class=\"tip-text\"><b>connections:</b> " + count + "</div>"; | |
} | |
}, | |
// Add node events | |
Events: { | |
enable: true, | |
enableForEdges: true, | |
type: 'Native', | |
//Change cursor style when hovering a node | |
onMouseEnter: function() { | |
fd.canvas.getElement().style.cursor = 'move'; | |
}, | |
onMouseLeave: function() { | |
fd.canvas.getElement().style.cursor = ''; | |
}, | |
//Update node positions when dragged | |
onDragMove: function(node, eventInfo, e) { | |
var pos = eventInfo.getPos(); | |
if (!node.nodeTo) { | |
node.pos.setc(pos.x, pos.y); | |
} else { | |
var p0 = node.nodeFrom.pos.getc(true); | |
// var p1 = node.getData("controlPoint"); | |
var p2 = node.nodeTo.pos.getc(true); | |
//var dist = distToQuadraticBezier(pos.x, pos.y, | |
// p0.x, p0.y, p1.x, p1.y, p2.x, p2.y); | |
var m = { | |
x: pos.x - (p0.x + p2.x)/2, | |
y: pos.y - (p0.y + p2.y)/2 | |
}; | |
var newControlPoint = { | |
x: pos.x + m.x, | |
y: pos.y + m.y | |
}; | |
node.setData("controlPoint", newControlPoint); | |
} | |
fd.plot(); | |
}, | |
//Implement the same handler for touchscreens | |
onTouchMove: function(node, eventInfo, e) { | |
$jit.util.event.stop(e); //stop default touchmove event | |
this.onDragMove(node, eventInfo, e); | |
}, | |
//Add also a click handler to nodes | |
onClick: function(node, eventInfo, evt) { | |
if((!node) && (!fd.animating)) { | |
var name = genNodeName(); | |
node = fd.graph.addNode( | |
{ | |
id: name, name: name | |
}); | |
node.pos.setc(eventInfo.getPos().x, eventInfo.getPos().y); | |
fd.plot(); | |
return; | |
} | |
if (node.nodeTo) return; | |
// Build the right column relations list. | |
// This is done by traversing the clicked node connections. | |
var html = "<h4>" + node.name + "</h4><b> connections:</b><ul><li>", | |
list = []; | |
node.eachAdjacency(function(adj){ | |
list.push(adj.nodeTo.name); | |
}); | |
//append connections information | |
$jit.id('inner-details').innerHTML = html + list.join("</li><li>") + "</li></ul>"; | |
} | |
}, | |
//Number of iterations for the FD algorithm | |
iterations: 200, | |
//Edge length | |
levelDistance: 130, | |
// Add text to the labels. This method is only triggered | |
// on label creation and only for DOM labels (not native canvas ones). | |
onCreateLabel: function(domElement, node){ | |
domElement.innerHTML = node.name; | |
var style = domElement.style; | |
style.fontSize = "0.8em"; | |
style.color = "#ddd"; | |
}, | |
// Change node styles when DOM labels are placed | |
// or moved. | |
onPlaceLabel: function(domElement, node){ | |
var style = domElement.style; | |
var left = parseInt(style.left); | |
var top = parseInt(style.top); | |
var w = domElement.offsetWidth; | |
style.left = (left - w / 2) + 'px'; | |
style.top = (top + 10) + 'px'; | |
style.display = ''; | |
} | |
}); | |
// load JSON data. | |
fd.loadJSON(json); | |
// compute positions incrementally and animate. | |
fd.animating = false; | |
fd.computeIncrementalOptions = { | |
iter: 40, | |
property: 'end', | |
onStep: function(perc){ | |
Log.write(perc + '% loaded...'); | |
fd.animating = true; | |
}, | |
onComplete: function(){ | |
Log.write('done'); | |
fd.animate({ | |
onComplete: function() { | |
fd.animating = false; | |
}, | |
modes: ['linear'], | |
transition: $jit.Trans.Elastic.easeOut, | |
duration: 2500 | |
}); | |
} | |
}; | |
fd.computeIncremental(fd.computeIncrementalOptions); | |
fd.periodicRefresh = function() { | |
fd.plot(); | |
setTimeout(fd.periodicRefresh, 1000); | |
}; | |
fd.periodicRefresh(); | |
// end | |
$jit.id("complete-graph-button").onclick = function() { | |
var nodes = new Array(); | |
var count = 0; | |
fd.graph.eachNode(function(n) { | |
nodes[count++] = n; | |
}); | |
for (var i = 0; i < count; i++) { | |
for (var j = i + 1; j < count; j++) { | |
fd.graph.addAdjacence(nodes[i], nodes[j], {}); | |
} | |
} | |
fd.plot(); | |
}; | |
$jit.id("compute-button").onclick = function() { | |
fd.computeIncremental(fd.computeIncrementalOptions); | |
fd.plot(); | |
}; | |
$jit.id("refresh-button").onclick = function() { | |
fd.plot(); | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment