Skip to content

Instantly share code, notes, and snippets.

@ZJONSSON
Last active May 18, 2023 08:52
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save ZJONSSON/1691430 to your computer and use it in GitHub Desktop.
Save ZJONSSON/1691430 to your computer and use it in GitHub Desktop.
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