Automatic floating labels using d3 force-layout
(function() { | |
d3.force_labels = function force_labels() { | |
var labels = d3.layout.force(); | |
// Update the position of the anchor based on the center of bounding box | |
function updateAnchor() { | |
if (!labels.selection) return; | |
labels.selection.each(function(d) { | |
var bbox = this.getBBox(), | |
x = bbox.x + bbox.width / 2, | |
y = bbox.y + bbox.height / 2; | |
d.anchorPos.x = x; | |
d.anchorPos.y = y; | |
// If a label position does not exist, set it to be the anchor position | |
if (d.labelPos.x == null) { | |
d.labelPos.x = x; | |
d.labelPos.y = y; | |
} | |
}); | |
} | |
//The anchor position should be updated on each tick | |
labels.on("tick.labels", updateAnchor); | |
// This updates all nodes/links - retaining any previous labelPos on updated nodes | |
labels.update = function(selection) { | |
labels.selection = selection; | |
var nodes = [], links = []; | |
selection[0].forEach(function(d) { | |
if(d && d.__data__) { | |
var data = d.__data__; | |
if (!d.labelPos) d.labelPos = {fixed: false}; | |
if (!d.anchorPos) d.anchorPos = {fixed: true}; | |
// Place position objects in __data__ to make them available through | |
// d.labelPos/d.anchorPos for different elements | |
data.labelPos = d.labelPos; | |
data.anchorPos = d.anchorPos; | |
links.push({target: d.anchorPos, source: d.labelPos}); | |
nodes.push(d.anchorPos); | |
nodes.push(d.labelPos); | |
} | |
}); | |
labels | |
.stop() | |
.nodes(nodes) | |
.links(links); | |
updateAnchor(); | |
labels.start(); | |
}; | |
return labels; | |
}; | |
})(); |
<!DOCTYPE html> | |
<html> | |
<head> | |
<script src="http://d3js.org/d3.v2.js"></script> | |
<script type="text/javascript" src="force_labels.js"></script> | |
<style> | |
.anchor { fill:blue} | |
.labelbox { fill:black;opacity:0.8} | |
.labeltext { fill:white;font-weight:bold;text-anchor:middle;font-size:16;font-family: serif} | |
.link { stroke:gray;stroke-width:0.35} | |
</style> | |
</head> | |
<body> | |
<div style="width:150px;float:left"> | |
<span id="corr-label">Correlation: </span><br> | |
<input type="range" min="-1.0" max="1.0" value="0.0" id="corr" step="0.01"/> | |
</div> | |
<div style="width:150px;float:left"> | |
<span id="charge-label">Label charge: </span><br> | |
<input type="range" min="0" max="100" value="60.0" id="charge" step="1"/> | |
</div> | |
<button type="button" id="addone">Add one measurement</button> | |
<button type="button" id="randomize20">Replace with 20</button> | |
<button type="button" id="randomize50">Replace with 50</button> | |
<button type="button" id="randomize100">Replace with 100</button> | |
<script type="text/javascript"> | |
var w=960,h=500, | |
x_mean = w/2, | |
x_std = w/10, | |
y_mean = h/2, | |
y_std = h/10, | |
labelBox,link, | |
data=[]; | |
var svg=d3.select("body") | |
.append("svg:svg") | |
.attr("height",h) | |
.attr("width",w) | |
function refresh() { | |
// plot the data as usual | |
anchors = svg.selectAll(".anchor").data(data,function(d,i) { return i}) | |
anchors.exit().attr("class","exit").transition().duration(1000).style("opacity",0).remove() | |
anchors.enter().append("circle").attr("class","anchor").attr("r",4).attr("cx",function(d) { return d.x}).attr("cy",function(d) { return h-d.y}) | |
anchors.transition() | |
.delay(function(d,i) { return i*10}) | |
.duration(1500) | |
.attr("cx",function(d) { return d.x}) | |
.attr("cy",function(d) { return h-d.y}) | |
// Now for the labels | |
anchors.call(labelForce.update) // This is the only function call needed, the rest is just drawing the labels | |
labels = svg.selectAll(".labels").data(data,function(d,i) { return i}) | |
labels.exit().attr("class","exit").transition().delay(0).duration(500).style("opacity",0).remove() | |
// Draw the labelbox, caption and the link | |
newLabels = labels.enter().append("g").attr("class","labels") | |
newLabelBox = newLabels.append("g").attr("class","labelbox") | |
newLabelBox.append("circle").attr("r",11) | |
newLabelBox.append("text").attr("class","labeltext").attr("y",6) | |
newLabels.append("line").attr("class","link") | |
labelBox = svg.selectAll(".labels").selectAll(".labelbox") | |
links = svg.selectAll(".link") | |
labelBox.selectAll("text").text(function(d) { return d.num}) | |
} | |
function redrawLabels() { | |
labelBox | |
.attr("transform",function(d) { return "translate("+d.labelPos.x+" "+d.labelPos.y+")"}) | |
links | |
.attr("x1",function(d) { return d.anchorPos.x}) | |
.attr("y1",function(d) { return d.anchorPos.y}) | |
.attr("x2",function(d) { return d.labelPos.x}) | |
.attr("y2",function(d) { return d.labelPos.y}) | |
} | |
// Initialize the label-forces | |
labelForce = d3.force_labels() | |
.linkDistance(0.0) | |
.gravity(0) | |
.nodes([]).links([]) | |
.charge(-60) | |
.on("tick",redrawLabels) | |
// and now for the data functionality | |
function randomize(count) { | |
z1=d3.random.normal() | |
z2=d3.random.normal() | |
data=data.concat(d3.range(count || 100).map(function(d,i) { return {z1:z1(),z2:z2(),num:data.length+i}})) | |
correlate() | |
} | |
function correlate() { | |
var corr = d3.select("#corr").property("value") | |
d3.select("#corr-label").text("Correlation: "+d3.format("%")(corr)) | |
data.forEach(function(d) { d.x = x_mean+(d.z1*x_std), | |
d.y = y_mean+y_std*(corr*d.z1+d.z2*Math.sqrt(1-Math.pow(corr,2)))}) | |
refresh() | |
} | |
// and finally hook up the controls | |
d3.select("#randomize20").on("click",function() { data=[];randomize(20)}) | |
d3.select("#randomize50").on("click",function() { data=[];randomize(50)}) | |
d3.select("#randomize100").on("click",function() { data=[];randomize(100)}) | |
d3.select("#addone").on("click",function() { randomize(1)}) | |
d3.select("#corr") | |
.on("change",function() { d3.select("#corr-label").text("Correlation: "+d3.format("%")(this.value))}) | |
.on("mouseup",correlate) | |
d3.select("#charge") | |
.on("change",function() { | |
d3.select("#charge-label").text("Label charge: "+d3.format("f")(this.value)) | |
labelForce.charge(-this.value).start() | |
}) | |
randomize() | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment