Skip to content

Instantly share code, notes, and snippets.

@shinaisan
Created October 13, 2012 13:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shinaisan/3884593 to your computer and use it in GitHub Desktop.
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.
<!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>
/* -*- 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;
}
/* -*- 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