Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Show D3 link text right-side up

I have built a D3 force directed visualization with text labels along the links. The one problem I'm running into is these labels appearing upside down when the links are to the left of their source node. Example here:

enter image description here

The code where I position the path and text looks like so:

var nodes = flatten(data);
var links = d3.layout.tree().links(nodes);

var path = vis.selectAll('path.link')
  .data(links, function(d) {
    return d.target.id;
  });

path.enter().insert('svg:path')
  .attr({
    class: 'link',
    id: function(d) {
      return 'text-path-' + d.target.id;
    },
    'marker-end': 'url(#end)'
  })
  .style('stroke', '#ccc');

var linkText = vis.selectAll('g.link-text').data(links);

linkText.enter()
  .append('text')
    .append('textPath')
      .attr('xlink:href', function(d) {
        return '#text-path-' + d.target.id;
      })
      .style('text-anchor', 'middle')
      .attr('startOffset', '50%')
      .text(function(d) {return d.target.customerId});

I know I will need to somehow determine the current angle of each path and then set the text position accordingly, but I am not sure how to.

Here is a link to a block based on this issue: http://blockbuilder.org/MattDionis/5f966a5230079d9eb9f4

The answer below has got me about 90% of the way there. Here is what my original visualization looks like with text longer than a couple digit number:

enter image description here

...and here is what it looks like utilizing the tips in the below answer:

enter image description here

So while the text is now "right-side up", it no longer follows the arc.

like image 553
MattDionis Avatar asked Dec 07 '15 19:12

MattDionis


1 Answers

The arcs you draw are such that their tangent in the middle is exactly the direction of the baseline of the text, AND it is also colinear with the vector that separates the two tree nodes.

We can use that to solve the problem.

A bit of math is needed. First, let's define a function that returns the angle of a vector v with respect to the horizontal axis:

function xAngle(v) {
    return Math.atan(v.y/v.x) + (v.x < 0 ? Math.PI : 0);
}

Then, at each tick, let's rotate the text in place by minus the angle of its baseline. First, a few utility functions:

function isFiniteNumber(x) {
    return typeof x === 'number' && (Math.abs(x) < Infinity);
}

function isVector(v) {
    return isFiniteNumber(v.x) && isFiniteNumber(v.y);
}

and then, in your tick function, add

linkText.attr('transform', function (d) {
    // Checks just in case, especially useful at the start of the sim
    if (!(isVector(d.source) && isVector(d.target))) {
        return '';
    }

    // Get the geometric center of the text element
    var box = this.getBBox();
    var center = {
        x: box.x + box.width/2,
        y: box.y + box.height/2
    };

    // Get the tangent vector
    var delta = {
        x: d.target.x - d.source.x,
        y: d.target.y - d.source.y
    };

    // Rotate about the center
    return 'rotate('
        + (-180/Math.PI*xAngle(delta))
        + ' ' + center.x
        + ' ' + center.y
        + ')';
    });
});

edit: added pic:

example result

edit 2 With straight lines instead of curved arcs (simply <text> instead of <textPath> inside of <text>), you can replace the part of the tick function that concerns linkText with this:

linkText.attr('transform', function(d) {
    if (!(isVector(d.source) && isVector(d.target))) {
        return '';
    }

    // Get the geometric center of this element
    var box = this.getBBox();
    var center = {
        x: box.x + box.width / 2,
        y: box.y + box.height / 2
    };

    // Get the direction of the link along the X axis
    var dx = d.target.x - d.source.x;

    // Flip the text if the link goes towards the left
    return dx < 0
        ? ('rotate(180 '
            + center.x
            + ' ' + center.y
            + ')')
        : '';
});

and this is what you get:

rotated text

Notice how the text gets flipped as the link goes from pointing more to the right to pointing more to the left.

The problem with this is that the text ends up below the link. That can be fixed as follows:

linkText.attr('transform', function(d) {
    if (!(isVector(d.source) && isVector(d.target))) {
        return '';
    }

    // Get the geometric center of this element
    var box = this.getBBox();
    var center = {
        x: box.x + box.width / 2,
        y: box.y + box.height / 2
    };

    // Get the vector of the link
    var delta = {
        x: d.target.x - d.source.x,
        y: d.target.y - d.source.y
    };

    // Get a unitary vector orthogonal to delta
    var norm = Math.sqrt(delta.x * delta.x + delta.y * delta.y);
    var orth = {
        x: delta.y/norm,
        y: -delta.x/norm
    };

    // Replace this with your ACTUAL font size
    var fontSize = 14;

    // Flip the text and translate it beyond the link line
    // if the link goes towards the left
    return delta.x < 0
        ? ('rotate(180 '
            + center.x
            + ' ' + center.y
            + ') translate('
            + (orth.x * fontSize) + ' '
            + (orth.y * fontSize) + ')')
        : '';
});

and now the result looks like this:

enter image description here

As you can see, the text sits nicely on top of the line, even when the link points towards the left.

Finally, in order to solve the problem while keeping the arcs AND having the text right side up curved along the arc, I reckon you would need to build two <textPath> elements. One for going from source to target, and one for going the opposite way. You would use the first one when the link goes towards the right (delta.x >= 0) and the second one when the link goes towards the left (delta.x < 0) and I think the result would look nicer and the code would not be necessarily more complicated than the original, just with a bit more logic added.

like image 122
jrsala Avatar answered Oct 28 '22 14:10

jrsala