I am currently working on a project where I need to keep the nodes of a force simulation away from the svg edges.
My attempt was using a exponential curve to increase the centering force for nodes reaching a certain x coordinate.
I am using this function
function getBorderForce(x, width, middle, steepness = 10) {
return Math.pow(((x-middle)/(width/2)), steepness)
}
If the given x value reaches middle + width / 2 it returns a value of 1. The steepness of the increase can modified by steepness parameter. A higher value means the nodes would get repealed later. I hope it is understandable.
I would then implement this function as follows into a d3.forceSimulation():
const sim = d3.forceSimulation().
.force("forceX", d3.forceX().x(d => d.forceX).strength(0.02))
.force("forceY", d3.forceY().y(d => d.forceY).strength(0.1))
.force("edgeRepeal", d3.forceX().x(_ => center).strength(d =>
getBorderForce(d.x, width, center)
))
.nodes(nodes)
When I use this approach my browser crashes and I get an out of memory error.
It works is if I use the d.forceX instead of the d.x value, but that means that nodes closer to the edges would be pushed in and would not be placed on their desired position d.forceX.
Maybe someone has a better idea or can show the flaw in my code – I am relatively new to d3, so it would be no surprise.
All the Best
Related
I use d3-force to lay out a graph with about 360 nodes.
const simulation = d3.forceSimulation(nodes)
.force(
'charge',
d3.forceManyBody()
.distanceMax(200)
.strength(-50)
)
.force(
'link',
d3.forceLink(links)
.id((d) => d.id)
.distance(30)
)
.force(
'center',
d3.forceCenter(
$svg.innerWidth() / 2,
$svg.innerHeight() / 2,
)
);
this looks good with all nodes visible – but there will also be the possibility to filter/remove nodes, in which case I would want the graph to be way more compact than it actually is (see animation).
this is probably due to the fact that there are no edges between the remaining nodes, and the fact that they are already spread out a lot when the new simulation starts.
while I could simply reset all node positions to the center of the canvas, that would not look great transition-wise. ideally each node would move from its current position to its new position in a more compact layout.
is there a way to achieve this?
I thought maybe the forceManyBody strength could transition from a positive value (attraction) at first to a negative value (repulsion), but apparently this value is can only be set once for the run of the simulation.
adding an attraction force (https://github.com/ericsoco/d3-force-attract) works well enough.
I have a simple force diagram that looks like this:
In the picture we can see nodes nicely aligned along side the x axis with a small 100px buffer zone to the left and the right of the middle (black) node. This is handled by 2 scales:
Left scale with range (10, width/2 - 100). 10 gives a small padding from the edge.
Right scale with range (width/2 + 100, width -10).
The position of the node in the middle is (width/2, height/2). The x position of red and green nodes is handled by the right/left scale as well as the axis at the bottom.
The above works fine until I add
.force("center", d3.forceCenter(width / 2, height / 2));
to my force definition and the result is:
It offsets the picture to the left by x amount. Why is that?
The svg element that the nodes are appended to doesnt have any css styling (padding or margins), maybe it should? What am I missing here?
UPDATE:
Here is Fiddle with the same problem. You can see that the right and left nodes are slightly off the number it should be on top of. If you would comment out line 40 the offset is no longer.
Whilst putting this together I realised that the problem might be how my diagram's svg is constructed. At the moment its
svg
|
- g.nodes
- g.links
- g.axisleft
- g.axisright
Nodes are appended into g.nodes element.
It is the center of gravity of the nodes
The chart center X is 400.
The node positions are
x1=168.68
x2=388.68
x3=642.68
Difference to 400
x1= -231.32
x2= -11.32
x3= 242.68
If you add them up it is very close to 0.
Force layout does not necessarily guarantee that the node will end up in a given starting coordinate. You are applying multiple forces to nodes and the nodes' final position is basically the result of superimposing all these forces.
In your case, the final positions are the result of interaction between forceCenter and force.X. You can remove one of them and see how the other one alone works.
One way to fix the problem in your case is to use the symmetrical nature of forceCenter:
.force('x', d3.forceX().x(function(d) {
if (d.cl === 0)
return width / 2;
if (d.cl < 0)
return xScale_lo(-5);
return xScale_hi(5); //instead of 6
I am not sure how this will work when the number of nodes increases. If the goal is to have separated clusters of nodes, you can experiment with cluster forces.
Let's say I have 16 circles in an 2 x 8 grid:
svg = d3.select(body).append('svg').attr('height,h).attr('width',w);
svg.selectAll('.centroids')
.data(d3.range(0,16))
.enter()
.append('circle')
.attr('class','centroids')
.attr('r','5')
.attr('cx', function(d,i) { return i * 10; })
.attr('cy', function(d,i) {
if (i > 7) return 20;
return 10;
});
Given a random coordinate in that space, how do I determine the nearest .centroid point?
One way in N time is of course to loop through all the points, measuring the hypotenuse to the difference in x and y coordinates, choosing the smallest value.
I'd like to find a better way though. Does anyone know an optimized way?
Optimization will depend on your exact settings:
if you have a few nodes (16 as in your example), in random positions, then your method is probably optimal (just compute the square of the hypotenuse, which gains a few square root operations).
if you have many nodes in random positions, you'll want to start considering quadtrees to manage your nodes. The overhead is not negligible, so don't bother about it until you have hundreds or thousands or nodes. On the plus side, d3has it all coded for you.
for a grid:
var startx=0;
var offsetx=10;
var cols=8;
var starty=10;
var offsety=10;
var rows=2;
var xi=d3.median([0,cols-1, Math.round((x-startx)/stepx)])
var yi=d3.median([0,rows-1, Math.round((y-starty)/stepy)])
var i=xi + yi*cols
this is constant time, adjust the (many) constants according to your dimensions.
A bit of details: (x-startx)/stepx allows to scale the coordinates so that the first dot is at 0, the next at 1, etc. Math.round gives the nearest integer, d3.median pushes the result between 0 and cols-1 (check out each case, by all mean it's nicer than nested ifs).... overall this gives the index of the nearest column, then you do the same for the rows, and there you are!
I am drawing circles by setting a fixed x position but a changing y position. The problem is the circles are overlapping since the radius of each circle is different.
Ideally in theory to solve that I would probably want to get the y position of the previous circle and add the radius of the current circle to it to get the y position of the current circle. Correct me if I am thinking it wrong.
Right now I am doing something like this now
var k = 10;
var circleAttributes = circles.attr("cx", '150')
.attr("cy", function (d) {
return (k++) * 10; //this is a very gray area
})
And I am getting an overlap. Ideally I would like to space the circles form each other. Even if the outer edges touch each other I could live with that. How should I approach it?
I am writing a range which i am using to get the radius
var rScale = d3.scale.linear()
.domain([min, max])
.range([10, 150]);
and simply passing that as the radius like this
.attr("r", function(d) { return rScale(d.consumption_gj_);})
This is my fiddle
http://jsfiddle.net/sghoush1/Vn7mf/27/
Did a solution here: http://tributary.io/inlet/6283630
The key was to keep track of the sum of the radius of all previous circles. I did that in a forEach loop:
data.forEach(function(d,i){
d.radius = rScale(d.consumption_gj_);
if (i !== 0){
d.ypos = d.radius*2 + data[i-1].ypos;
}
else {
d.ypos = d.radius*2;
}
})
then, when setting the attributes of the circles you can use your new d.radius and d.ypos
var circleAttributes = circles.attr("cx", '150')
.attr("cy", function (d,i) {
return d.ypos + 5*i;
})
.attr("r", function(d) { return d.radius;})
The Charge Property
The charge in a force layout refers to how nodes in the environment push away from one another or attract one another. Kind of like magnets, nodes have a charge that can be positive (attraction force) or negative (repelling force).
From the Documentation:
If charge is specified, sets the charge strength to the specified value. If charge is not specified, returns the current charge strength, which defaults to -30. If charge is a constant, then all nodes have the same charge. Otherwise, if charge is a function, then the function is evaluated for each node (in order), being passed the node and its index, with the this context as the force layout; the function's return value is then used to set each node's charge. The function is evaluated whenever the layout starts.
A negative value results in node repulsion, while a positive value results in node attraction. For graph layout, negative values should be used; for n-body simulation, positive values can be used. All nodes are assumed to be infinitesimal points with equal charge and mass. Charge forces are implemented efficiently via the Barnes–Hut algorithm, computing a quadtree for each tick. Setting the charge force to zero disables computation of the quadtree, which can noticeably improve performance if you do not need n-body forces.
A good tutorial that will help you see this in action:
http://vallandingham.me/bubble_charts_in_d3.html
I'm trying to create a visualization with D3 such that nodes are differently sized by a particular attribute and bigger nodes go to the center and smaller nodes go to the outside. I have sizing and clustering and collision detection working, but I can't figure out how to tell the bigger nodes to go to the center.
I've tried messing with the charge, but couldn't convince that to work. I got linkDistance to move the bigger ones to the center, but (a) getting there was VERY jittery and (b) the smaller ones are way outside rather than tightly packed. The linkDistance is still in the code, just commented out.
It's up at http://pokedex.mrh.is/stats/index.html:
The relevant code (I assume) is also below. The nodes are sized per their attr attribute. Oh, and the nodes are Pokémon.
force = d3.layout.force()
// .gravity(0.05)
// .charge(function(d, i) { return d.attr; })
// .linkDistance(function(d) {
// return 50000/Math.pow(d.source.attr+d.target.attr,1);
// })
.nodes(pokemon)
// .links(links)
.size([$(window).width(), $(window).height()]);
The following gave me a less jittery version of what you have now.
force = d3.layout.force()
.gravity(0.1)
.charge(function(d, i) { return -d[selectedAttr]})
.friction(0.9)
.nodes(pokemon)
.size([$(window).width(), $(window).height()]);
To answer your actual question, each node's coordinates are currently being placed in your graph at random. I quote from the D3 documentation:
When nodes are added to the force layout, if they do not have x and y attributes already set, then these attributes are initialized using a uniform random distribution in the range [0, x] and [0, y], respectively.
From my experience, there's no magic force method that gets the nodes you want to the center of the map. The way that I've accomplished your desired result in the past has been by replacing the randomized coordinates of each node with coordinates that place the nodes in a the desired order, expanding from the center of the map.