This demonstrates how to use d3-annotation() with bboxCollide
to procedurally place node labels. After using the nodes data to create a network visualization of the Les Miserables play, we filter the nodes to leave out the side characters and pass that array to d3-annotation
. We then create a second forceSimulation
, this time using the size of the notes as the property in our bounding box collision detection, to move the labels out of each others' way.
d3-annotation
by Susie Lu.
forked from emeeks's block: Network Annotation with Collision Detection
xxxxxxxxxx
<html lang="en">
<head>
<meta charset="utf-8">
<link href='https://fonts.googleapis.com/css?family=Lato:300,900' rel='stylesheet' type='text/css'>
<style>
:root {
--annotation-color: #e91e56;
}
body{
background-color: whitesmoke;
}
svg {
background-color: white;
font-family: 'Lato';
overflow: visible;
}
line {
stroke:#e3e3e3;
}
.editable .annotation-subject, .editable .annotation-textbox {
cursor: move;
}
.line {
fill: none;
stroke: black;
stroke-width: 1px;
}
.annotation path {
stroke: var(--annotation-color);
fill: rgba(0,0,0,0);
}
.annotation path.connector-arrow{
fill: var(--annotation-color);
}
.annotation text {
fill: var(--annotation-color);
}
.annotation-title {
font-weight: bold;
}
.annotation .annotation-subject circle.handle {
display: none;
}
.annotation-note-bg {
fill: rgba(255, 255, 255, 0);
}
circle.handle {
stroke-dasharray: 5;
stroke: grey;
fill: rgba(255, 255, 255, 0);
cursor: move;
stroke-opacity: .4;
}
circle.handle.highlight {
stroke-opacity: 1;
}
.annotation.major {
font-weight: 900;
font-size: 1em;
}
.annotation-note-label tspan {
text-anchor: middle;
}
</style>
</head>
<body>
<svg width=1000 height=650></svg>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="bboxCollide.js"></script>
<script src="https://cdn.jsdelivr.net/gh/susielu/d3-annotation/d3-annotation.js"></script>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var color = d3.scaleOrdinal(d3.schemeCategory20)
.range(["#e91e56", "#00965f", "#00bcd4", "#3f51b5", "#9c27b0", "#ff5722", "#cddc39", "#607d8b", "#8bc34a"]);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id( d => d.id ))
.force("charge", d3.forceManyBody().strength(-80))
.force("center", d3.forceCenter(width / 2, height / 2));
d3.json("miserables.json", function(error, graph) {
if (error) throw error;
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("stroke-width", d => Math.sqrt(d.value));
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", d => d.type === "major" ? 9 : 3)
.style("fill", d => d3.hsl(color(d.group)).darker())
.style("fill-opacity", d => d.type === "other" ? 0.5 : 1)
node.append("title")
.text(d => d.id);
window.collide = d3.bboxCollide((a) => {
return [[a.offsetCornerX - 5, a.offsetCornerY - 10],[a.offsetCornerX + a.width + 5, a.offsetCornerY + a.height+ 5]]
})
.strength(0.5)
.iterations(1)
window.yScale = d3.scaleLinear()
simulation
.nodes(graph.nodes)
.on("tick", ticked)
.on("end", function() {
const noteBoxes = makeAnnotations.collection().noteNodes
window.labelForce = d3.forceSimulation(noteBoxes)
.force("x", d3.forceX(a => a.positionX).strength(a => Math.max(0.25, Math.min(3, Math.abs(a.x - a.positionX) / 20))))
.force("y", d3.forceY(a => a.positionY).strength(a => Math.max(0.25, Math.min(3, Math.abs(a.x - a.positionX) / 20))))
.force("collision", window.collide)
.alpha(0.5)
.on('tick', d => {
makeAnnotations.annotations()
.forEach((d, i) => {
const match = noteBoxes[i]
d.dx = match.x - match.positionX
d.dy = match.y - match.positionY
})
makeAnnotations.update()
})
})
const nonOtherNodes = graph.nodes
.filter(d => d.type !== "other")
simulation.force("link")
.links(graph.links);
function ticked() {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
makeAnnotations.annotations()
.forEach((d, i) => {
d.position = nonOtherNodes[i]
})
}
window.makeAnnotations = d3.annotation()
.type(d3.annotationLabel)
.annotations(nonOtherNodes
.map((d,i) => {
return {
data: {x: d.x, y: d.y, group: d.group},
note: { label: d.id,
align: "middle",
orientation: "fixed" },
connector: { type: "elbow"},
className: d.type
}
}))
.accessors({ x: d => d.x , y: d => d.y})
svg.append("g")
.attr("class", "annotation-test")
.call(makeAnnotations)
svg.selectAll(".annotation-note text")
.style("fill", d => color(d.data.group))
svg.selectAll(".annotation-connector > path")
.style("stroke", (d,i) => color(d.data.group))
});
</script>
</body>
</html>
Updated missing url https://cdn.rawgit.com/susielu/d3-annotation/master/d3-annotation.js to https://cdn.jsdelivr.net/gh/susielu/d3-annotation/d3-annotation.js
https://d3js.org/d3.v4.js
https://cdn.rawgit.com/susielu/d3-annotation/master/d3-annotation.js