OK, so I thought I'd give it a shot and implement this with some vector math, it's prettier and the result is reusable.
A few clarifications:
- A "vector" is simply two numbers (x and y)
- "Coordinates" are in structure identical to vectors, they just mean something different to us. We can run the same math on the though.
- A "positioned vector" is two vectors (like source and destination)
- You can "free" a positioned vector by subtracting the first vector from the second one (you get a new vector which is no longer anchored in the coordinate system)
- The "length" of a vector is calculated using pythagoras theorem (also called norm)
- "Vector addition" is simply adding the the xs and ys of two or more vectors, resulting in a new vector.
- "Scalar multiplication" and division is done by dividing x and y with the scalar
- A "unit vector" is the vector divided by its length
Assuming we want this to to work dynamically ("per tick"), the initial links adjustment looks like this (I am using coffeescript):
links.attr('x1', ({source,target}) -> source.x)
.attr('y1', ({source,target}) -> source.y)
.attr('x2', ({source,target}) -> target.x)
.attr('y2', ({source,target}) -> target.y)
What we want to do is move the source and target nodeRadius
away from the circle. For that we use vector math to
- free the positioned vector (the link consists of two coordinates, we want a single unanchored vector)
- calculate the unit vector of the free vector
- once we have that, we can multiply it by
nodeRadius
. This new vector represents the distance between the node center and its border, with the same direction as the link.
- add the vector to the source coordinates, these new coordinates will be on the edge of the circle.
OK, so we will use the following functions to do this:
length = ({x,y}) -> Math.sqrt(x*x + y*y)
sum = ({x:x1,y:y1}, {x:x2,y:y2}) -> {x:x1+x2, y:y1+y2}
diff = ({x:x1,y:y1}, {x:x2,y:y2}) -> {x:x1-x2, y:y1-y2}
prod = ({x,y}, scalar) -> {x:x*scalar, y:y*scalar}
div = ({x,y}, scalar) -> {x:x/scalar, y:y/scalar}
unit = (vector) -> div(vector, length(vector))
scale = (vector, scalar) -> prod(unit(vector), scalar)
free = ([coord1, coord2]) -> diff(coord2, coord1)
This might look a little overwhelming, it's very compact because coffeescript allows us to deconstruct things directly in the method signature, quite handy!
As you can see there is another function called scale
. It's simply a convenience function to combine steps 2. & 3.
Now let's try and set the new x coordinate for the link source. Remember: The coordinate should be moved by nodeRadius
, so that it starts on the border of the circle instead of inside it.
(d) ->
# Step 1
freed = free(d)
# Step 2
unit = unit(freed)
# Step 3
scaled = prod(unit, nodeRadius)
# Step 2+3 would be scale(freed, nodeRadius)
# Step 4, coords are pretty much just vectors,
# so we just use the sum() function to move the source coords
coords = sum(d.source, scaled)
return coords.x
Nothing to it! Putting all of that into the tick()
function, we get:
links.attr('x1', ({source,target}) -> sum(source, scale(free([source,target]), nodeRadius)).x)
.attr('y1', ({source,target}) -> sum(source, scale(free([source,target]), nodeRadius)).y)
.attr('x2', ({source,target}) -> diff(target, scale(free([source,target]), nodeRadius)).x)
.attr('y2', ({source,target}) -> diff(target, scale(free([source,target]), nodeRadius)).y)
Oh, and don't forget to subtract from the target coordinates, otherwise you'd just be making the line longer again (i.e. moving it by nodeRadius
).