|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>Force based labels for fixed nodes</title> |
|
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script> |
|
<style> |
|
svg circle { |
|
opacity: .5; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<script> |
|
var outerWidth = 960, |
|
outerHeight = 500, |
|
padding = {top: 60, right: 60, bottom: 60, left: 60}; |
|
|
|
var width = outerWidth - padding.left - padding.right, |
|
height = outerHeight - padding.top - padding.bottom; |
|
|
|
// randomly generate points, o.w., input your own |
|
var points = [], npoints = 50; |
|
for(var i=0; i < npoints; i++) |
|
points.push({x: Math.random()*width, y: Math.random()*height, label: "node" + i}); |
|
|
|
// create label nodes with forced layout |
|
var labels = [], |
|
labelLinks = []; |
|
|
|
for(var i = 0; i < points.length; i++) { |
|
var node = { |
|
label: points[i].label, |
|
x: points[i].x, |
|
y: points[i].y |
|
}; |
|
labels.push({node : node }); labels.push({node : node }); // push twice |
|
labelLinks.push({ source : i * 2, target : i * 2 + 1, weight : 1, x: 100 }); |
|
}; |
|
|
|
var force = d3.layout.force() |
|
.nodes(labels) |
|
.links(labelLinks) |
|
.gravity(0) |
|
.linkDistance(0) |
|
.linkStrength(8) |
|
.charge(-100) |
|
.size([width, height]) |
|
.on("tick", tick); |
|
|
|
function tick() { |
|
circleNode.call(updateNode); |
|
labelNode.each(function(d, i) { |
|
if(i % 2 == 0) { |
|
d.x = d.node.x; |
|
d.y = d.node.y; |
|
} else { |
|
var b = this.childNodes[1].getBBox(); |
|
var diffX = d.x - d.node.x, |
|
diffY = d.y - d.node.y; |
|
var dist = Math.sqrt(diffX * diffX + diffY * diffY); |
|
var shiftX = Math.min(0, b.width * (diffX - dist) / (dist * 2)); |
|
var shiftY = 5; |
|
this.childNodes[1].setAttribute("transform", "translate(" + shiftX + "," + shiftY + ")"); |
|
} |
|
}); |
|
labelNode.call(updateNode); |
|
} |
|
|
|
// draw svg |
|
var svg = d3.select("body") |
|
.append("svg") |
|
.attr("width", outerWidth) |
|
.attr("height", outerHeight) |
|
.append("g") |
|
.attr("transform", "translate(" + padding.left + "," + padding.top + ")"); |
|
|
|
var circleNode = svg.selectAll("circle") |
|
.data(points) |
|
.enter().append("circle") |
|
.attr("class", "node") |
|
.attr("r", 5) |
|
.style("fill", "#555") |
|
.style("stroke-width", 3); |
|
|
|
var labelNode = svg.selectAll("g") |
|
.data(force.nodes()) |
|
.enter().append("g") |
|
.attr("class", "labelNode"); |
|
|
|
labelNode.append("circle") |
|
.attr("r", 0) |
|
.style("fill", "red"); |
|
|
|
labelNode.append("text") |
|
.text(function(d, i) { return i % 2 == 0 ? "" : d.node.label }) |
|
.style("fill", "#555") |
|
.style("font-size", 12); |
|
|
|
// Update nodes |
|
var updateNode = function() { |
|
this.attr("transform", function(d) { |
|
return "translate(" + d.x + "," + d.y + ")"; |
|
}); |
|
} |
|
|
|
force.start(); |
|
/* // comment out for no animations |
|
var niters = 50; |
|
for (var i = niters; i > 0; --i) force.tick(); |
|
force.stop(); |
|
//*/ |
|
</script> |
|
</body> |
|
</html> |