Convert d3.js bubbles into forced/gravity based layout - javascript

I have a set of data that I am visualizing using d3.js. I am representing data points in the form of bubbles, where the configuration for bubbles is as follows:
var dot = svg.selectAll("g")
.data(data)
.enter()
.append("g");
dot.append("circle")
.attr("class", "dot")
.attr("cx", function(d) { return xp(x(d)); })
.attr("cy", function(d) { return yp(y(d)); })
.style("fill", function(d) { return colorp(color(d)); })
.attr("r", function(d) { return radiusp(radius(d)*2000000); });
dot.append("text")
.attr("x", function(d) { return xp(x(d)); })
.attr("y", function(d) { return yp(y(d)); })
.text(function(d) { return d.name; })
Where xp, yp, colorp and radiusp are defined as follows:
var xp = d3.scale.log().domain([300, 1e5]).range([0, width]),
yp = d3.scale.linear().domain([10, 85]).range([height, 0]),
radiusp = d3.scale.sqrt().domain([0, 5e8]).range([0, 40]),
colorp = d3.scale.category10();
At this point, the bubbles are being displayed as static on their positions (where position is defined by xp and yp), while the size of the bubble is basically coming from radiusp and color is defined by colorp.
Right now I am showing them exactly as this example:
http://bl.ocks.org/mbostock/4063269
What I need is to display them in this form:
http://jsfiddle.net/andycooper/PcjUR/1/
That is: They should be packed using gravity function, have some charge, can be dragged and repel each other to some extent. I can see that there is a way through d3.layout.force() but not really able to integrate that into this.. I will be really thankful if you can suggest me the right path or some working example or even a hint. Thank you.

I think you were almost there but the specification of your dot variable is not the best one. I would transform it like this:
var dot = svg.selectAll(".dot")
.data(data)
.enter()
Afterwards, once the circles have been plotted, what you do is that you create a force layout, instantiate it with the nodes you just created, add a on("tick") method, and then start the layout. An example is the following:
var force = d3.layout.force().nodes(data).size([width, height])
.gravity(0)
.charge(0)
.on("tick", function(e){
dot
.each(gravity(.2 * e.alpha))
.each(collide(.5))
.attr("cx", function (d) {return d.x;})
.attr("cy", function (d) {return d.y;});
})
.start();
To have a complete answer, I will add also the gravity and collide methods from your fiddle (with adjusted variable names)
function gravity(alpha) {
return function (d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
function collide(alpha) {
var padding = 6
var quadtree = d3.geom.quadtree(dot);
return function (d) {
var r = d.r + radiusp.domain()[1] + padding,
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.r + quad.point.r + (d.color !== quad.point.color) * padding;
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;
});
};
}
I think the problem you had was that perhaps you were applying the force layout to the g element of each of the circles, which unfortunately was not working. I hope this will give you an idea how to proceed. Your last line of the dot declaration was adding a g element for each circle, which was a little difficult to handle.
Thanks.
PS I assume that the x, y, and r attributes of your data contain the x,y, and radius.

Related

Updating data in a Clustered Force Layout D3 bubble Chart

Morning !
I'm working on a bubble chart in D3.js, I'm quite close of what I need,
but I've got a behavior problem when updating data (changing set).
I would like the bubbles to stay in place, and update size and position without the bouncy effect due to colision :(
function convertData(m){
var nodes = m;
nodes.forEach(function(d) {
clusters[d.cluster] = d;
d.radius = Math.ceil(Math.sqrt(d.pop/Math.PI))*10;
});
return nodes;
}
function updateAnnee(annee){
nodes = convertData(annee);
var svg = d3.select("svg")
var node = svg.selectAll("circle")
.data(nodes)
node.transition()
.duration(0)
.attr('r', function(d){ return d.radius});
node
.enter().append("circle")
node
.exit().remove();
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
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-50; })
}
// 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;
});
};
}
}
What I have so far is viewable here : https://jsfiddle.net/hf998do7/1/
This is based on the work of Mr Bostock http://bl.ocks.org/mbostock/7881887
Thanks

D3 Force Layout with Image as Node but Wrong Images are Appended

I'm trying to create a force-layout with images as nodes that can be modified depending on user input (through a couple of checkboxes). However, when user is changing his/her input and the layout is redrawn, some images are showing up on top of the wrong node and I can't understand why.
The way I structured my code:
I pulled data from a csv
I created an "update" function that creates/modifies the data array depending user input
I created a "draw" function that draws the force layout every time the user changes his/her input (i.e. every time the function "update" is called)
The first part of my code seems to work fine. The array of data is created from the csv and dynamically updated correctly.
But when the graph is redrawn some images appear on the wrong node (even though the image showing up in the tooltip is the right one..).
Below is my "draw" function, I think that's where the problem is.
function draw(nodes) {
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.on("tick", tick)
.charge(-0.01)
.gravity(0)
.start();
function tick(e) {
// Set initial positions
nodes.forEach(function(d) {
d.radius = radius;
});
var node = svg.selectAll(".node")
.data(nodes);
var newNode = node.enter().append("g")
.attr("class", "node")
.on("click", function(d) {
div.transition().duration(300).style("opacity", 1);
div.html("<img src=img_med/" + d.name + ".png >")
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY-10) + "px");
})
.on("mouseout", function (d) { div.transition().delay(1500).duration(300).style("opacity", 0);});
newNode.append("image")
.attr("xlink:href", function(d) { return "img_med/" + d.name + ".png"; })
.attr("x", -8)
.attr("y", -8)
.attr("width", 30)
.attr("height", 30)
.style("cursor", "pointer")
.call(force.drag);
node.exit().remove();
node.each(moveTowardDataPosition(e.alpha));
node.each(collide(e.alpha));
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}
function moveTowardDataPosition(alpha) {
return function(d) {
d.x += (x(d[xVar]) - d.x) * 0.1 * alpha;
d.y += (y(d[yVar]) - d.y) * 0.1 * alpha;
};
}
// Resolve collisions between nodes.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + radius + padding,
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 + padding;
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;
});
};
}
}
Any help/suggestions/pointers to help me understand why the wrong images show up on top of those nodes would be much appreciated!
Let me know if anything is unclear.
Thanks a lot!
Ok, I was just missing a key function to tell D3 how to match existing nodes and data.
I replaced
var node = svg.selectAll(".node")
.data(nodes);
by
var node = svg.selectAll(".node")
.data(nodes, function(d) {return d.name;});
and it worked.
Thanks Lars!

Understanding a d3 Multi-Foci Force Layout

I'm trying to understand how this beautiful example works...
http://bl.ocks.org/mbostock/1804919
I see that clustering is done by the color of the nodes, but I'm confused by the line in question in the collision detection function...
r = d.radius + quad.point.radius + (d.color !== quad.point.color) * padding;
How can you "add" the product of a comparison of the colors "d.color" and "quad.point.color"? I would have assumed this would return nothing more than a true/false? Either way, I'm not sure I follow only this reference to color will have the desired effect of clustering by color?
Anyway, I haven't been able to find any line-by-line description of the workings of the collision detection function, so I'm really hoping that someone here understands it well enough to help explain this bit to me.
All I'm ultimately trying to achieve is to adapt the example to cluster by another non-numeric node attribute (e.g. d.person_name !== quad.point.person_name).
Thanks!
The line you are asking about is calculating the allowable distance between nodes, the distance between nodes (l) is compared to r to determine if there is a collision between d and quad.point. The value padding is added to the allowable distance between nodes if they are of the same colour. The boolean result is coerced into a Number type by the context.
Instead of assuming what JS does its really easy to open the browser tools and just type the expression in to see what the result is...
But the collision detection is not involved in the clustering, that is handled by this code...
// Move nodes toward cluster focus.
function gravity(alpha) {
return function(d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
If you have some data and you want to use the same code to group them by a particular attribute, then you need to add a cx and cy property to your data such that items with the same attribute value (the value does not need to be numeric) have the same cx and cy values.
Example (modified version of this)
var width = 600,
height = 200,
padding = 6, // separation between nodes
maxRadius = 6;
var n = 200, // total number of nodes
names = ["Givens", "Crowder", "Lannister", "Baratheon", "Stark"],
m = names.length; // number of distinct clusters
var color = d3.scale.category10()
.domain(d3.range(m));
var x = d3.scale.ordinal()
.domain(names)
.rangePoints([0, width], 1),
legend = d3.svg.axis()
.scale(x)
.orient("top")
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
v = (i + 1) / m * -Math.log(Math.random());
return {
radius: Math.sqrt(v) * maxRadius,
color: color(i),
cx: x(names[i]),
cy: height / 2
};
});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(0)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height),
gLegend = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + height * 0.9 + ")")
.call(legend);
gLegend.selectAll(".tick text")
.attr("fill", function(d, i) {
return color(i);
});
var circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", function(d) {
return d.radius;
})
.style("fill", function(d) {
return d.color;
})
.call(force.drag);
function tick(e) {
circle
.each(gravity(.2 * e.alpha))
.each(collide(.5))
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
// Move nodes toward cluster focus.
function gravity(alpha) {
return function(d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
// Resolve collisions between nodes.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + padding,
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.color !== quad.point.color) * padding;
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;
});
};
}
circle {
stroke: #000;
}
.x.axis path {
fill: none;
}
.x.axis text {
font-family: Papyrus, Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, sans-serif;
}
body {
background-color: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>

adding text to circles in a rectangle

I am trying to draw circles in a rectangular div. I have followed the advice from Question 13339615(the answer I used is also made available in this fiddle, and this works perfectly.
However, being completely new to d3, I cannot work out how to label the circles. What I would basically like to recreate is similar to the visualisation in this article.
I have tried the following modifications to the fiddle:
var bubbles = bubbleGroup.selectAll("circle")
.data(data)
.enter()
.append("circle")
.append("text").attr("dy", ".3em")
.style("text-anchor", "middle").text("test");
but this breaks the visualisation.
Following question 13615381 I have also tried things like:
var bubbles = bubbleGroup.selectAll("circle")
.data(data)
.enter()
.append("circle");
bubbleGroup.append("text")
.attr("dx", function(d){return -20})
.text(function(d){return "test"})
but the text does not display. I'm imaging the code should be some variation of these, but I cannot figure it out.
Thank you!
Fixed by putting the circle and text inside a g and adjusting the g css-transform.
JSFiddle
var bubbles = bubbleGroup.selectAll("g")
.data(data)
.enter().append("g").attr("class","gBubble");
bubbles.append("circle")
.on("mouseover",function(){
$(this).attr("cursor","pointer")
})
.on("click",function(){alert("clicked")});
bubbles.append("text").text(function(d){return d.name;}).style("opacity","1");
(function() {
//D3 program to fit circles of different sizes
//in a rectangle of fixed aspect ratio
//as tightly as reasonable.
//
//By Amelia Bellamy-Royds, in response to
//http://stackoverflow.com/questions/13339615/packing-different-sized-circles-into-rectangle-d3-js
//Based on Mike Bostock's
//"http://bl.ocks.org/mbostock/7882658" example:
//http://bl.ocks.org/mbostock/7882658
//parameters//
var N = 25; //number of nodes
var sortOrder = -1;
//>0 for ascending, <0 for descending, 0 for no sort
//create data array//
var data = [], i = N;
var randNorm = d3.random.normal(1,0.6);
while(i--) data.push({
"size": Math.max(randNorm(), 0.1) });
//circle area will be proportional to size
var dataMax = d3.max(data, function(d){return d.size;});
var totalSize = d3.sum(data, function(d){return d.size;});
//________________//
//Set up SVG and rectangle//
var svg = d3.select("svg");
var digits = /(\d*)/;
var margin = 50; //space in pixels from edges of SVG
var padding = 4; //space in pixels between circles
var svgStyles = window.getComputedStyle(svg.node());
var width = parseFloat(svgStyles["width"]) - 2*margin;
var height = parseFloat(svgStyles["height"]) - 2*margin;
var usableArea = Math.PI*
Math.pow( Math.min(width,height)/2 ,2)*0.667;
var scaleFactor = Math.sqrt(usableArea)/
Math.sqrt(totalSize)/Math.PI;
var rScale = d3.scale.sqrt()
//make radius proportional to square root of data r
.domain([0, dataMax]) //data range
.range([0, Math.sqrt(dataMax)*scaleFactor]);
//The rScale range will be adjusted as necessary
//during packing.
//The initial value is based on scaling such that the total
//area of the circles is 2/3 the area of the largest circle
//you can draw within the box.
/*
console.log("Dimensions: ", [height, width]);
console.log("area", width*height);
console.log("Usable area: ", usableArea);
console.log("TotalSize: ", totalSize);
console.log("Initial Scale: ", scaleFactor);
console.log("RScale: ",rScale.domain(), rScale.range());
console.log("r(1)", rScale(1) );
// */
var box = svg.append("rect")
.attr({ "height": height, "width":width,
"x":margin, "y":margin,
"class":"box"
});
var bubbleGroup = svg.append("g")
.attr("class", "bubbles")
.attr("transform",
"translate(" + [margin,margin] + ")");
//__Initialize layout objects__//
// Use the pack layout to initialize node positions:
d3.layout.pack()
.sort((
sortOrder?
( (sortOrder<0)?
function(a,b){return b.size - a.size;} : //descending
function(a,b){return a.size - b.size;} ) : //ascending
function(a,b){return 0;} //no sort
))
.size([width/scaleFactor, height/scaleFactor])
.value(function(d) { return d.size; })
.nodes({children:data});
//Use the force layout to optimize:
var force = d3.layout.force()
.nodes(data)
.size([width/scaleFactor, height/scaleFactor])
.gravity(.5)
.charge(0) //don't repel
.on("tick", updateBubbles);
//Create circles!//
var bubbles = bubbleGroup.selectAll("circle")
.data(data)
.enter()
.append("circle");
//Create text
var text = bubbleGroup.selectAll("text")
.data(data).enter().append("text")
.attr("dy", function(d){
return d.y;
})
.attr("dx", function(d){
return d.x;
}).style("text-anchor", "middle").text("test");
// Create a function for this tick round,
// with a new quadtree to detect collisions
// between a given data element and all
// others in the layout, or the walls of the box.
//keep track of max and min positions from the quadtree
var bubbleExtent;
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
var maxRadius = Math.sqrt(dataMax);
var scaledPadding = padding/scaleFactor;
var boxWidth = width/scaleFactor;
var boxHeight = height/scaleFactor;
//re-set max/min values to min=+infinity, max=-infinity:
bubbleExtent = [[Infinity, Infinity],[-Infinity, -Infinity]];
return function(d) {
//check if it is pushing out of box:
var r = Math.sqrt(d.size) + scaledPadding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
if (nx1 < 0) {
d.x = r;
}
if (nx2 > boxWidth) {
d.x = boxWidth - r;
}
if (ny1 < 0) {
d.y = r;
}
if (ny2 > boxHeight) {
d.y = boxHeight - r;
}
//check for collisions
r = r + maxRadius,
//radius to center of any possible conflicting nodes
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 = Math.sqrt(d.size) + Math.sqrt(quad.point.size)
+ scaledPadding;
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;
});
//update max and min
r = r-maxRadius; //return to radius for just this node
bubbleExtent[0][0] = Math.min(bubbleExtent[0][0],
d.x - r);
bubbleExtent[0][1] = Math.min(bubbleExtent[0][1],
d.y - r);
bubbleExtent[1][0] = Math.max(bubbleExtent[1][0],
d.x + r);
bubbleExtent[1][1] = Math.max(bubbleExtent[1][1],
d.y + r);
};
}
function updateBubbles() {
bubbles
.each( collide(0.5) ); //check for collisions
text.each(collide(0.5));//check for text collisions
//update the scale to squeeze in the box
//to match the current extent of the bubbles
var bubbleWidth = bubbleExtent[1][0] - bubbleExtent[0][0];
var bubbleHeight = bubbleExtent[1][1] - bubbleExtent[0][1];
scaleFactor = (height/bubbleHeight +
width/bubbleWidth)/2; //average
/*
console.log("Box dimensions:", [height, width]);
console.log("Bubble dimensions:", [bubbleHeight, bubbleWidth]);
console.log("ScaledBubble:", [scaleFactor*bubbleHeight,
scaleFactor*bubbleWidth]);
//*/
rScale
.range([0, Math.sqrt(dataMax)*scaleFactor]);
//shift the bubble cluster to the top left of the box
bubbles
.each( function(d){
d.x -= bubbleExtent[0][0];
d.y -= bubbleExtent[0][1];
});
//update positions and size according to current scale:
bubbles
.attr("r", function(d){return rScale(d.size);} )
.attr("cx", function(d){return scaleFactor*d.x;})
.attr("cy", function(d){return scaleFactor*d.y;});
text
.attr("dy", function(d){
return (scaleFactor*d.y)+4;
})
.attr("dx", function(d){
return scaleFactor*d.x*2;
});
}
force.start();
})();
rect.box {
fill:none;
stroke:royalblue;
stroke-width:5;
shape-rendering: crispEdges;
}
g.bubbles circle {
fill:rgba(255,0,64,0.5);
stroke:rgb(255,0,64);
stroke-width:3;
}
g.bubbles text {
fill:royalblue;
font-family:sans-serif;
text-anchor:middle;
alignment-baseline:middle;
opacity:1;
pointer-events:all;
transition:1s;
}
g.bubbles text:hover {
opacity:1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg height=500 width=500></svg>
I've added text to circles, and also collision behavior too.
Initially the text is invisible because in the CSS they mentioned like below
g.bubbles text {
fill:royalblue;
font-family:sans-serif;
text-anchor:middle;
alignment-baseline:middle;
opacity:0;//See this value, this makes text to invisible
pointer-events:all;
transition:1s;
}
g.bubbles text:hover {
opacity:1;
}
In my snippet I changed it visible by making it's opacity to 1.
And updated fiddle

D3 force layout: Moving arrows along links depending on node radius

In this D3 example:
nodes have variable radius. I managed to move node labels so that they are always just next to their circles.
However, how to move arrows? (You can see that for Microsoft, Apple etc, they are almost covered by the circle)
Related questions:
here
here
here
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