Calling a function to feed an .attr() in d3 - javascript

I have .attr that create my link paths in d3 as below, Lines 42 in the demo:
link.each(function (d){})
.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;
});
The above draws my path links. I've changed this to calculate making the link paths smaller. However, there is a lot of repeated logic and not sure how to call a function to return my the values required. As below:
link.attr("x1", function(d) {
// Total difference in x and y from source to target
let diffX = d.target.x - d.source.x;
let diffY = d.target.y - d.source.y;
// Length of path from center of source node to center of target node
let pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
// x and y distances from center to outside edge of target node
let offsetX = (diffX * 40) / pathLength;
return d.source.x + offsetX;
}).attr("y1", function(d) {
let diffX = d.target.x - d.source.x;
let diffY = d.target.y - d.source.y;
let pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
let offsetY = (diffY * 40) / pathLength;
return d.source.y + offsetY;
}).attr("x2", function(d) {
let diffX = d.target.x - d.source.x;
let diffY = d.target.y - d.source.y;
let pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
let offsetX = (diffX * 40) / pathLength;
return d.target.x - offsetX;
}).attr("y2", function(d) {
let diffX = d.target.x - d.source.x;
let diffY = d.target.y - d.source.y;
let pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
let offsetY = (diffY * 40) / pathLength;
return d.target.y - offsetY;
})
It returns offsetX for d.source.x/d.target.x values and offsetY for d.source.y/d.target.y.
Ideally i dont want all this repeated logic and have tried doing a .call() on the link which goes into a function but then I dont know how to return the result into the attributes themselves e.g.
.attr('x1', function (d) {
return d.source.x; //function return from .call()
})
I've also tried putting the function call within the above attribute but get an error of "is not a function" e.g
.attr('x1', function (d) {
return this.myNewPathFunction(d);
})
Lines 42 in the demo

Your objective is both writing less lines of repetitive code and reducing the amount of calculations needed.
One of the idiomatic D3 solutions is using selection.each();
Thus, in your case:
link.each(d, i, n) {
// Total difference in x and y from source to target
let diffX = d.target.x - d.source.x;
let diffY = d.target.y - d.source.y;
// Length of path from center of source node to center of target node
let pathLength = Math.sqrt(diffX * diffX + diffY * diffY);
// x and y distances from center to outside edge of target node
let offsetX = (diffX * 40) / pathLength;
let offsetY = (diffY * 40) / pathLength;
d3.select(n[i])
.attr("x1", d.source.x + offsetX)
.attr("y1", d.source.y + offsetY)
.attr("x2", d.target.x - offsetX)
.attr("y2", d.target.y - offsetY);
};
Note that the computation of offsetX and offsetY happens only once per element. In your specific case it's not a heavy computation, but when it is it's a good idea reducing the burden on the browser.

You could move your logic to a function like so:
const coordinate = (position, d, index, nodes) => {
// Total difference in x and y from source to target
let diffX = d.target.x - d.source.x;
let diffY = d.target.y - d.source.y;
// Length of path from center of source node to center of target node
let pathLength = Math.sqrt(diffX * diffX + diffY * diffY);
// x and y distances from center to outside edge of target node
let offsetX = (diffX * 40) / pathLength;
let offsetY = (diffY * 40) / pathLength;
if (position === 'x1') return d.source.x + offsetX;
if (position === 'y1') return d.source.y + offsetY;
if (position === 'x2') return d.target.x - offsetX;
if (position === 'y2') return d.target.y - offsetY;
};
link
.attr('x1', (d, i, nodes) => coordinate('x1', d, i, nodes))
.attr('y1', (d, i, nodes) => coordinate('y1', d, i, nodes))
.attr('x2', (d, i, nodes) => coordinate('x2', d, i, nodes))
.attr('y2', (d, i, nodes) => coordinate('y2', d, i, nodes));

Related

How to change the color of two circles upon overlap?

Hello I would like to know how it would be possible two make it that two circles change color when they overlap. Preferably the section that is overlapped would become white since its meant to represent sets.
var canvas = d3.select("canvas"),
context = canvas.node().getContext("2d"),
width = canvas.property("width"),
height = canvas.property("height"),
radius = 32;
var circles = d3.range(4).map(function(i) {
return {
index: i,
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
render();
canvas.call(d3.drag()
.subject(dragsubject)
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
.on("start.render drag.render end.render", render));
function render() {
context.clearRect(0, 0, width, height);
for (var i = 0, n = circles.length, circle; i < n; ++i) {
circle = circles[i];
context.beginPath();
context.moveTo(circle.x + radius, circle.y);
context.arc(circle.x, circle.y, radius, 0, 2 * Math.PI);
context.fillStyle = color(circle.index);
context.fill();
if (circle.active) {
context.lineWidth = 2;
context.stroke();
}
}
}
function dragsubject() {
for (var i = circles.length - 1, circle, x, y; i >= 0; --i) {
circle = circles[i];
x = circle.x - d3.event.x;
y = circle.y - d3.event.y;
if (x * x + y * y < radius * radius) return circle;
}
}
function dragstarted() {
circles.splice(circles.indexOf(d3.event.subject), 1);
circles.push(d3.event.subject);
d3.event.subject.active = true;
}
function dragged() {
d3.event.subject.x = d3.event.x;
d3.event.subject.y = d3.event.y;
}
function dragended() {
d3.event.subject.active = false;
}
<canvas width="800" height="500"></canvas>
<script src="//d3js.org/d3.v4.min.js"></script>
My ideal solution would be something that allow me to change the color of the overlapping section to another color to represent the intersection between 2 sets.
Thank you in advance
Edit: some updates have been made however Ive only found how to do the coloring for static elements instead of moving
var x1 = 100,
y1 = 100,
x2 = 150,
y2 = 150,
r = 70;
var svg = d3.select('svg')
.append('svg')
.attr('width', 500)
.attr('height', 500);
svg.append('circle')
.attr('cx', x1)
.attr('cy', y1)
.attr('r', r)
.style('fill', 'steelblue')
.style("fill-opacity",0.5)
.style("stroke","black");
svg.append('circle')
.attr('cx', x2)
.attr('cy', y2)
.attr('r', r)
.style('fill', 'orange')
.style("fill-opacity",0.5)
.style("stroke","black");
var interPoints = intersection(x1, y1, r, x2, y2, r);
svg.append("g")
.append("path")
.attr("d", function() {
return "M" + interPoints[0] + "," + interPoints[2] + "A" + r + "," + r +
" 0 0,1 " + interPoints[1] + "," + interPoints[3]+ "A" + r + "," + r +
" 0 0,1 " + interPoints[0] + "," + interPoints[2];
})
.style('fill', 'red')
.style("fill-opacity",0.5)
.style("stroke","black");
function intersection(x0, y0, r0, x1, y1, r1) {
var a, dx, dy, d, h, rx, ry;
var x2, y2;
/* dx and dy are the vertical and horizontal distances between
* the circle centers.
*/
dx = x1 - x0;
dy = y1 - y0;
/* Determine the straight-line distance between the centers. */
d = Math.sqrt((dy * dy) + (dx * dx));
/* Check for solvability. */
if (d > (r0 + r1)) {
/* no solution. circles do not intersect. */
return false;
}
if (d < Math.abs(r0 - r1)) {
/* no solution. one circle is contained in the other */
return false;
}
/* 'point 2' is the point where the line through the circle
* intersection points crosses the line between the circle
* centers.
*/
/* Determine the distance from point 0 to point 2. */
a = ((r0 * r0) - (r1 * r1) + (d * d)) / (2.0 * d);
/* Determine the coordinates of point 2. */
x2 = x0 + (dx * a / d);
y2 = y0 + (dy * a / d);
/* Determine the distance from point 2 to either of the
* intersection points.
*/
h = Math.sqrt((r0 * r0) - (a * a));
/* Now determine the offsets of the intersection points from
* point 2.
*/
rx = -dy * (h / d);
ry = dx * (h / d);
/* Determine the absolute intersection points. */
var xi = x2 + rx;
var xi_prime = x2 - rx;
var yi = y2 + ry;
var yi_prime = y2 - ry;
return [xi, xi_prime, yi, yi_prime];
}
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
<svg width="500" height="500"></svg>
^This works for statics
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 32;
var circles = d3.range(4).map(function() {
return {
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
svg.selectAll("circle")
.data(circles)
.enter().append("circle")
.style("fill-opacity",0.3)
.style("stroke","black")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 60)
.style("fill", function(d, i) { return color(i); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("active", false);
}
<svg width="500" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
^This is my moving circles that I would like to add said effect on.
Is there any way to combine the two codes to achieve this ?
Thanks again
You can use the intersection function of your static approach (second snippet) inside the dragged function of your dynamic approach (third snippet).
First of all, let's create 2 groups, so the "intersection" path will always be in front of the circles:
var g1 = svg.append("g");
var g2 = svg.append("g");
Now to the important part.
Inside the dragged function, get the position of the other (non-dragged) circle:
var otherCircle = circles.filter(function(e, j) {
return i !== j;
}).datum();
If you have more than two circles you'll have to refactor this, but my demo below has just two circles, so let's move on.
Then, check if they overlap:
Math.hypot(d.x - otherCircle.x, d.y - otherCircle.y) < 2 * radius
If they do, call intersection, and set the path's d attribute:
var interPoints = intersection(d.x, d.y, radius, otherCircle.x, otherCircle.y, radius);
path.attr("d", function() {
return "M" + interPoints[0] + "," + interPoints[2] + "A" + radius + "," + radius +
" 0 0,1 " + interPoints[1] + "," + interPoints[3] + "A" + radius + "," + radius +
" 0 0,1 " + interPoints[0] + "," + interPoints[2];
})
If they don't, erase the path:
path.attr("d", null)
Here is the working demo:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 60;
var data = d3.range(2).map(function(d, i) {
return {
x: i ? 200 : 400,
y: 150
};
});
var g1 = svg.append("g");
var g2 = svg.append("g");
var color = d3.scaleOrdinal()
.range(d3.schemeCategory10);
var circles = g1.selectAll("circle")
.data(data)
.enter().append("circle")
.style("fill-opacity", 0.3)
.style("stroke", "black")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", radius)
.style("fill", function(d, i) {
return color(i);
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var path = g2.append("path")
.style("fill", "white")
.style("stroke", "black")
.attr("d", null);
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
}
function dragged(d, i) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
var otherCircle = circles.filter(function(e, j) {
return i !== j;
}).datum();
if (Math.hypot(d.x - otherCircle.x, d.y - otherCircle.y) < 2 * radius) {
var interPoints = intersection(d.x, d.y, radius, otherCircle.x, otherCircle.y, radius);
path.attr("d", function() {
return "M" + interPoints[0] + "," + interPoints[2] + "A" + radius + "," + radius +
" 0 0,1 " + interPoints[1] + "," + interPoints[3] + "A" + radius + "," + radius +
" 0 0,1 " + interPoints[0] + "," + interPoints[2];
})
} else {
path.attr("d", null)
}
}
function dragended(d) {
d3.select(this).classed("active", false);
}
function intersection(x0, y0, r0, x1, y1, r1) {
var a, dx, dy, d, h, rx, ry;
var x2, y2;
/* dx and dy are the vertical and horizontal distances between
* the circle centers.
*/
dx = x1 - x0;
dy = y1 - y0;
/* Determine the straight-line distance between the centers. */
d = Math.sqrt((dy * dy) + (dx * dx));
/* Check for solvability. */
if (d > (r0 + r1)) {
/* no solution. circles do not intersect. */
return false;
}
if (d < Math.abs(r0 - r1)) {
/* no solution. one circle is contained in the other */
return false;
}
/* 'point 2' is the point where the line through the circle
* intersection points crosses the line between the circle
* centers.
*/
/* Determine the distance from point 0 to point 2. */
a = ((r0 * r0) - (r1 * r1) + (d * d)) / (2.0 * d);
/* Determine the coordinates of point 2. */
x2 = x0 + (dx * a / d);
y2 = y0 + (dy * a / d);
/* Determine the distance from point 2 to either of the
* intersection points.
*/
h = Math.sqrt((r0 * r0) - (a * a));
/* Now determine the offsets of the intersection points from
* point 2.
*/
rx = -dy * (h / d);
ry = dx * (h / d);
/* Determine the absolute intersection points. */
var xi = x2 + rx;
var xi_prime = x2 - rx;
var yi = y2 + ry;
var yi_prime = y2 - ry;
return [xi, xi_prime, yi, yi_prime];
}
svg {
background-color: wheat;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="600" height="300"></svg>
SVG mix-blend-mode hover from screen to normal
This isn't exactly what you're looking for since it doesn't let you programmatically control the color of intersecting segments, but CSS mix-blend-mode is a very simple solution I've used with d3. I've tried to accomplish the same thing but ran into performance problems calculating intersections on animating large datasets. Compatibility may be a concern if this needs to work in IE/ Edge, but otherwise most modes are supported in Chrome, Firefox, and Safari (even mobile).
Here's a good guide with examples on d3 as well and a simplified Codepen snippet.
Otherwise it seems like you've already found D3.js - detect intersection area. To get that to work with drag, you'll need to write the calculations to determine which circles are overlapping, then calculate their intersection area.

After a few clicks the animation freezes, why?

I have the following code, which I took and modify from here
Here is my jsfiddle in action
I am currently developing a user interaction that needs these bubbles, exactly 5 bubbles that gravitate to the center of the screen. The thing is that I know how many times a user will click on each of these bubbles. The thing I noticed is that at some point the bubbles keep growing but the collision among them will stop working.
Here you can see the code that modify:
var width = 500,
height = 500,
padding = 1.5, // separation between same-color circles
clusterPadding = 4, // separation between different-color circles
maxRadius = 40;
var n = 5, // total number of circles
m = 1; // number of distinct clusters
var color = d3.scale.category10()
.domain(d3.range(m));
// The largest node for each cluster.
var clusters = new Array(m);
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
//r = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
r = maxRadius,
d = {cluster: i, radius: r};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
return d;
});
// Use the pack layout to initialize node positions.
d3.layout.pack()
.sort(null)
.size([width, height])
.children(function(d) { return d.values; })
.value(function(d) { return d.radius * d.radius; })
.nodes({values: d3.nest()
.key(function(d) { return d.cluster; })
.entries(nodes)});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var node = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.style("fill", function(d) { return color(d.cluster); })
.on("click", function(d) {
d.radius *= 1.1;
d3.select(this).attr("r", d.radius);
})
.call(force.drag);
node.transition()
.duration(750)
.delay(function(d, i) { return i * 5; })
.attrTween("r", function(d) {
var i = d3.interpolate(0, d.radius);
return function(t) { return d.radius = i(t); };
});
function tick(e) {
node
.each(cluster(10 * e.alpha * e.alpha))
.each(collide(.5))
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// Move d to be adjacent to the cluster node.
function cluster(alpha) {
return function(d) {
var cluster = clusters[d.cluster];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + cluster.radius;
if (l != r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
};
}
// Resolves collisions between d and all other circles.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + Math.max(padding, clusterPadding),
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + (d.cluster === quad.point.cluster ? padding : clusterPadding);
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
That behaviour you described happens because the force simulation has ended. So, the simplest solution is reheating the force at every click:
.on("click", function(d) {
d.radius *= 1.1;
d3.select(this).attr("r", d.radius);
force.resume();
})
Here, force.resume():
Sets the cooling parameter alpha to 0.1. This method sets the internal alpha parameter to 0.1, and then restarts the timer. Typically, you don't need to call this method directly; it is called automatically by start. It is also called automatically by drag during a drag gesture.
Here is your updated fiddle: https://jsfiddle.net/usb7nhfm/

D3 Force Graph - Fixed Nodes that Don't Overlap

I am looking to develop a viz that consists of a node link graph. I have a series of points whose position I don't want to change unless there is a collision (one node on another) on the graph. In case of collided nodes, I want to space them so that they don't overlap. My JS code is as below
var chartWidth = 200;
var chartHeight = 200;
var widthPadding = 40;
var heightPadding = 40;
var link, node;
$(function(){
initialize();
});
function initialize() {
var jsonString = '{"nodes":[{"x":40,"y":64,"r":6,"fixed":true},{"x":40,"y":63,"r":6,"fixed":true},{"x":119,"y":53,"r":6,"fixed":true},{"x":119,"y":73,"r":6,"fixed":true},{"x":137,"y":73,"r":6,"fixed":true},{"x":140,"y":140,"r":6,"fixed":true},{"x":68,"y":57,"r":6,"fixed":true},{"x":70,"y":75,"r":6,"fixed":true},{"x":51,"y":59,"r":6,"fixed":true},{"x":51,"y":54,"r":6,"fixed":true},{"x":137,"y":40,"r":6,"fixed":true}],"links":[{"source":0,"target":1},{"source":1,"target":2},{"source":2,"target":3},{"source":3,"target":4},{"source":4,"target":5},{"source":0,"target":1},{"source":1,"target":6},{"source":6,"target":7},{"source":7,"target":4},{"source":4,"target":5},{"source":0,"target":1},{"source":1,"target":8},{"source":8,"target":9},{"source":9,"target":10},{"source":10,"target":5}]}';
drawForceDirectedNodeLink($.parseJSON(jsonString));
}
function drawForceDirectedNodeLink(graph){
var width = chartWidth + (2*widthPadding);
var height = chartHeight + (2*heightPadding);
var q = d3.geom.quadtree(graph.nodes),
i = 0,
n = graph.nodes.length;
while (++i < n) {
q.visit(collide(graph.nodes[i]));
}
var force = d3.layout.force()
.size([width, height])
.gravity(0.05)
.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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) { return d.r; });
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node");
}
function collide(node) {
var r = node.radius + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * .5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2
|| x2 < nx1
|| y1 > ny2
|| y2 < ny1;
};
}
As you can see, I have tried to implement the collision detection logic mentioned here. But some how I have not been able to get that part work.
Notice that within your jsonString declaration inside of initialize(), each node is being given an r property. However, further down within collide(), you're doing the following:
.attr("r", function(d) { return d.radius - 2; })
Make sure your nodes have a radius property attached to them. If not, the following change should do it:
.attr("r", function(d) { return d.r - 2; })
You can see on line 30 of Mike Bostock's script that his nodes are initially declared with a radius property, as opposed to your r property.
var nodes = d3.range(200).map(function() { return {radius: Math.random() * 12 + 4}; }),
UPDATE
Change node.radius to node.r and quad.point.radius to quad.point.r. And it should work. Looks like it was just a NaN problem.

D3: Show network reaching layout, then stop force

I'm trying to get my D3 network to freeze after it reaches a nice layout (alpha reaches 0). I want the force to stop completely, even when a node is dragged (the user should be able to rearrange the nodes manually). I think I know how to do the second part of this, by modifying the functions that are called on mousedown and mouseup for the nodes. However, I can't get the original layout and freezing to work.
I've looked at the examples for "static" force layouts, where the network is displayed only after the layout is completed. However, I want the network to display as it's reaching the stable layout. I added this to the end of the function that draws the network:
while (force.alpha() >0.005) {
force.tick();
}
force.stop();
With this addition, the network doesn't display until it gets to force.stop(). Does anyone know how I can get it to display while it's "ticking"?
EDIT: Here's my implementation of the tick function:
function tick(e) {
console.log(force.alpha());
if (force.alpha() <0.05) {
force.stop();
}
var h = svgH;
if (e.alpha < 0.05) {
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length;
while (++i < n) {
q.visit(collide(nodes[i], e.alpha));
}
}
path.attr("d", function(d) {
// Total difference in x and y from source to target
diffX = d.target.x - d.source.x;
diffY = d.target.y - d.source.y;
// Length of path from center of source node to center of target node
pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
// x and y distances from center to outside edge of target node
offsetX = (diffX * d.target.radius) / pathLength;
offsetY = (diffY * d.target.radius) / pathLength;
if (d.target.y < d.source.y) {
var avgY = (d.target.y + d.source.y)/2;
if (d.target.fixed != true) {
d.target.y = avgY;
}
if (d.source.fixed != true) {
d.source.y = avgY;
}
}
return "M" + d.source.x + "," + d.source.y + "L" + (d.target.x - offsetX) + "," + (d.target.y - offsetY);
});
// Keep circles within bounds of screen
var r = 6;
circle.attr("cx", function(d) { return d.x = Math.max(r + d.radius, Math.min(w - r, d.x)); })
.attr("cy", function(d) {
return d.y = Math.max(d.radius, Math.min(h - d.radius, d.y));
});
text.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
Check the stop condition in the tick event handler -- this way you can redraw the network on each tick and stop.

linking nodes of variable radius with arrows

I have some circles/nodes of different radius and I have to connect them with paths having arrow ends.
Here's the code for the marker:
svg.append("svg:defs").selectAll("marker")
.data(["default"])
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 5)
.attr("refY", -1.5)
.attr("markerWidth", 10)
.attr("markerHeight", 10)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M1,-5L10,0L0,5");
I have stored the radius of circles in an array.
Here's the screen shot:
The arrow is actually "inside" the circles. How do I get the arrows to be at the surface of the circles?
This is an old question, but here is my solution if you want your arrowheads to be at the edge of your nodes instead of on top of or beneath them. My approach was also to draw the path connecting the nodes such that the end points were on the nodes' edges rather than at the nodes' centers. Starting from the Mobile Patent Suits example (http://bl.ocks.org/mbostock/1153292), I replaced the linkArc method with:
function linkArc(d) {
var sourceX = d.source.x;
var sourceY = d.source.y;
var targetX = d.target.x;
var targetY = d.target.y;
var theta = Math.atan((targetX - sourceX) / (targetY - sourceY));
var phi = Math.atan((targetY - sourceY) / (targetX - sourceX));
var sinTheta = d.source.r * Math.sin(theta);
var cosTheta = d.source.r * Math.cos(theta);
var sinPhi = d.target.r * Math.sin(phi);
var cosPhi = d.target.r * Math.cos(phi);
// Set the position of the link's end point at the source node
// such that it is on the edge closest to the target node
if (d.target.y > d.source.y) {
sourceX = sourceX + sinTheta;
sourceY = sourceY + cosTheta;
}
else {
sourceX = sourceX - sinTheta;
sourceY = sourceY - cosTheta;
}
// Set the position of the link's end point at the target node
// such that it is on the edge closest to the source node
if (d.source.x > d.target.x) {
targetX = targetX + cosPhi;
targetY = targetY + sinPhi;
}
else {
targetX = targetX - cosPhi;
targetY = targetY - sinPhi;
}
// Draw an arc between the two calculated points
var dx = targetX - sourceX,
dy = targetY - sourceY,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + sourceX + "," + sourceY + "A" + dr + "," + dr + " 0 0,1 " + targetX + "," + targetY;
}
Note that this code expects an "r," or radius, attribute to be in the node data. To place the points of the arrows at the correct positions, I changed the refX and refY attributes so that the point of the arrow was at the edge of the node:
svg.append("defs").selectAll("marker")
.data(["suit", "licensing", "resolved"])
.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5");
This is really funny; I just solved this problem yesterday.
What I did is to end the path at the edge of the node, not at the centre.
My case is more complicated because I use Bezier curves, not straight lines, but this might help you:
svg.append("svg:defs").selectAll("marker")
.data(["default"])
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -3 6 6")
.attr("refX", 5.0)
.attr("refY", 0.0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-2.0L5,0L0,2.0");
links
.attr("fill", "none")
.attr("d", function(d) {
var tightness = -3.0;
if(d.type == "straight")
tightness = 1000;
// Places the control point for the Bezier on the bisection of the
// segment between the source and target points, at a distance
// equal to half the distance between the points.
var dx = d.target.x - d.source.x;
var dy = d.target.y - d.source.y;
var dr = Math.sqrt(dx * dx + dy * dy);
var qx = d.source.x + dx/2.0 - dy/tightness;
var qy = d.source.y + dy/2.0 + dx/tightness;
// Calculates the segment from the control point Q to the target
// to use it as a direction to wich it will move "node_size" back
// from the end point, to finish the edge aprox at the edge of the
// node. Note there will be an angular error due to the segment not
// having the same direction as the curve at that point.
var dqx = d.target.x - qx;
var dqy = d.target.y - qy;
var qr = Math.sqrt(dqx * dqx + dqy * dqy);
var offset = 1.1 * node_size(d.target);
var tx = d.target.x - dqx/qr* offset;
var ty = d.target.y - dqy/qr* offset;
return "M" + d.source.x + "," + d.source.y + "Q"+ qx + "," + qy
+ " " + tx + "," + ty; // to "node_size" pixels before
//+ " " + d.target.x + "," + d.target.y; // til target
});
By the way; you'll have to do the same for the 'source' arrow head (I only have it at the target)
you may order the svg elements such that the circles will be rendered first, the lines with arrows thereafter (in d3 there is a .ordermethod, see here for details. for the record, the corrsponding part of the raphael api is discussed here).
I search online, none of the answer worked, so I made my own:
Here is the code:
//arrows
svg.append("defs").selectAll("marker")
.data(["suit", "licensing", "resolved"])
.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 9)
.attr("refY", 0)
.attr("markerWidth", 10)
.attr("markerHeight", 10)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5 L10,0 L0, -5")
.style("stroke", "#4679BD")
.style("opacity", "0.6");
//Create all the line svgs but without locations yet
var link = svg.selectAll(".link")
.data(forceData.links)
.enter().append("line")
.attr("class", "link")
.style("marker-end", "url(#suit)");
//Set up the force layout
var force = d3.layout.force()
.nodes(forceData.nodes)
.links(forceData.links)
.charge(-120)
.linkDistance(200)
.size([width, height])
.on("tick", tick)
.start();
function tick(){
link.attr("x1", function (d) { return d.source.x; })
.attr("y1", function (d) { return d.source.y; })
.attr("x2", function (d) {
return calculateX(d.target.x, d.target.y, d.source.x, d.source.y, d.target.radius);
})
.attr("y2", function (d) {
return calculateY(d.target.x, d.target.y, d.source.x, d.source.y, d.target.radius);
});
d3.selectAll("circle")
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; });
d3.select("#forcelayoutGraph").selectAll("text")
.attr("x", function (d) { return d.x; })
.attr("y", function (d) { return d.y; });
}
function calculateX(tx, ty, sx, sy, radius){
if(tx == sx) return tx; //if the target x == source x, no need to change the target x.
var xLength = Math.abs(tx - sx); //calculate the difference of x
var yLength = Math.abs(ty - sy); //calculate the difference of y
//calculate the ratio using the trigonometric function
var ratio = radius / Math.sqrt(xLength * xLength + yLength * yLength);
if(tx > sx) return tx - xLength * ratio; //if target x > source x return target x - radius
if(tx < sx) return tx + xLength * ratio; //if target x < source x return target x + radius
}
function calculateY(tx, ty, sx, sy, radius){
if(ty == sy) return ty; //if the target y == source y, no need to change the target y.
var xLength = Math.abs(tx - sx); //calculate the difference of x
var yLength = Math.abs(ty - sy); //calculate the difference of y
//calculate the ratio using the trigonometric function
var ratio = radius / Math.sqrt(xLength * xLength + yLength * yLength);
if(ty > sy) return ty - yLength * ratio; //if target y > source y return target x - radius
if(ty < sy) return ty + yLength * ratio; //if target y > source y return target x - radius
}

Categories

Resources