I have a D3 force directed graph and each node displays a label. This label can vary in length from a single line of text to multiple lines of text. I have successfully created a multiline label for the nodes by referring to this post. However, I would like to vertically center the label over the node. I have not been able to reassign the label position to vertical center in a way that is effective across all label types (e.g. one line labels, two line labels, etc).
Essentially, I am unsure how to center the label as a whole while maintaining separation between each line of text in the label. Any assistance would be appreciated.
var node = svg.selectAll('.node')
.data(force.nodes())
.enter().append('circle')
.attr('class', 'node')
.attr('r', width * 0.01)
.style('fill', function(d) {return color(d.group); })
var maxLength = 20;
var separation = 12;
var textX = 0;
var text = svg.selectAll(".text")
.data(nodes)
.enter().append("text")
.style("text-anchor", "middle")
.each(function (d) {
var lines = wordwrap2(d.name, maxLength).split('\n');
for (var i = 0; i < lines.length; i++) {
d3.select(this)
.append("tspan")
.attr("dy", separation)
.attr("x", textX)
.text(lines[i]);
}});
function wordwrap2( str, width, brk, cut ) {
brk = brk || '\n';
width = width || 75;
cut = cut || false;
if (!str) { return str; }
var regex = '.{0,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)');
return str.match( RegExp(regex, 'g') ).join( brk );
};
Select the label and translate it up half its height, using lines.length
and separation
:
d3.select(this).attr("transform", "translate(0," + (separation*lines.length/2*-1) + ")");
Here is a demo:
<script src="https://d3js.org/d3.v2.min.js?2.9.3"></script>
<style>
.link {
stroke: #aaa;
}
.node text {
stroke: #333;
cursor: pointer;
}
.node circle {
stroke: #fff;
stroke-width: 3px;
fill: #555;
}
</style>
<body>
<script>
var width = 400,
height = 300
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.distance(50)
.charge(-3000)
.size([width, height]);
var json = {
"nodes": [{
"name": "node1"
}, {
"name": "node2"
}, {
"name": "node3"
}, {
"name": "node4"
}, {
"name": "node5 has a very very very very long long name"
}],
"links": [{
"source": 0,
"target": 1
}, {
"source": 0,
"target": 2
}, {
"source": 0,
"target": 3
}, {
"source": 0,
"target": 4
}]
};
force
.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", 2);
var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 8);
var maxLength = 10;
var separation = 12;
var textX = 0;
node.append("text")
.attr("dominant-baseline", "central")
.attr("dx", 12)
.attr("dy", ".35em")
.each(function (d) {
var lines = wordwrap2(d.name, maxLength).split('\n');
for (var i = 0; i < lines.length; i++) {
d3.select(this)
.append("tspan")
.attr("dy", separation)
.attr("x", textX)
.text(lines[i]);
d3.select(this).attr("transform", "translate(0," + (separation*lines.length/2*-1) + ")");
}});
function wordwrap2( str, width, brk, cut ) {
brk = brk || '\n';
width = width || 75;
cut = cut || false;
if (!str) { return str; }
var regex = '.{0,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)');
return str.match( RegExp(regex, 'g') ).join( brk );
};
force.on("tick", function() {
link.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
</script>