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:
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:
...and here is what it looks like utilizing the tips in the below answer:
So while the text is now "right-side up", it no longer follows the arc.
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:
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:
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With