Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Automatic floating labels using d3 force-layout
d3-force-labels.js
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v4.min.js"></script>
<!-- <script type="text/javascript" src="d3-force-labels.js"></script> -->
<!-- <script src="https://raw.githack.com/TylerRick/d3-force-labels/master/src/d3-force-labels.js"></script> -->
<script src="https://rawcdn.githack.com/TylerRick/d3-force-labels/master/src/d3-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
}
rect {
fill: none;
stroke: #aaa;
}
</style>
</head>
<body>
<div class="controls">
<div style="width:150px; float:left">
<span id="corr-label"></span><br>
<input type="range" min="-1.0" max="1.0" id="corr" step="0.01" />
</div>
<div style="width:150px; float:left">
<span id="charge-label"></span><br>
<input type="range" min="0" max="100" id="charge" step="1" value="60.0" />
</div>
<div style="width:150px; float:left">
<span id="collide-label"></span><br>
<input type="range" min="0" max="20" id="collide" step="1" />
</div>
<button type="button" id="add-one">Add one point</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>
</div>
<script type="text/javascript">
var width = 960, height = 500,
label_radius = 11,
x_mean = width/2,
x_std = width/10,
y_mean = height/2,
y_std = height/10,
labelBox, link,
data = [];
var svg = d3.select("body")
.append("svg:svg")
.attr("height", height)
.attr("width", width)
svg
.append("rect")
.attr("width", width - 1)
.attr("height", height - 1)
function refresh() {
// Plot the data
anchors = svg.selectAll(".anchor")
.data(data, function(d, i) { return i })
anchors.exit()
.attr("class", "exit")
.transition()
.duration(1000)
.style("opacity", 0)
.remove()
anchors = anchors.enter()
.append("circle")
.merge(anchors)
.attr("class", "anchor")
.attr("r", 4)
.attr("cx", function(d) { return d.x })
.attr("cy", function(d) { return height - 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 height - d.y })
// Now for the labels
anchors.call(labelSim.update)
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", label_radius)
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})
labelSim.alpha(1).restart()
}
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 })
}
// Functions to generate random data points
function randomize(count) {
z1 = d3.randomNormal()
z2 = d3.randomNormal()
data = data.concat(d3.range(count).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").data([corr])
updateControls()
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()
}
// Hook up the controls
function updateControls() {
d3.select("#corr-label").text(function(d) {
return "Correlation: " + d3.format(".1%")(d)
})
d3.select("#charge-label").text(function(d) {
return "Label charge: " + d3.format(".3")(d)
})
d3.select("#collide-label").text(function(d) {
return "Collide radius: " + d3.format(".3")(d)
})
}
function changeCharge(strength) {
labelSim.force('charge').strength(-strength)
d3.select("#charge").property("value", strength)
d3.select("#charge-label").data([strength])
updateControls()
}
function changeCollide(newRadius) {
if (newRadius <= 0) {
labelSim.force('collide', null)
} else {
labelSim.force('collide', d3.forceCollide(newRadius))
}
d3.select("#collide").property("value", newRadius)
d3.select("#collide-label").data([newRadius])
updateControls()
}
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("#add-one"). on("click", function() { randomize(1) })
d3.select("#corr")
.on("mouseup", correlate)
d3.select("#charge")
.on("change", function() {
changeCharge(this.value)
labelSim.alpha(0.2).restart()
})
d3.select("#collide")
.on("change", function() {
changeCollide(this.value)
labelSim.alpha(0.1).restart()
})
// Start the simulation
labelSim = d3.forceLabels()
.on("tick", redrawLabels)
randomize(100)
changeCharge(50)
changeCollide(0)
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment