javascriptd3.jssvglabelforce-layout

Vertically center a label consisting of multiple lines over a node in D3 force directed graph


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 );
};

Solution

  • 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>