I want to display a set of nodes in a force-directed graph.Those nodes have almost exclusively a parent-child relationship (almost a tree).There are a few nodes which have many children, and most others are leaves.
This produces a structure of several interconnected node "islands".The problem is, those "islands" keep spreading apart, specially when nodes are dragged.
Fiddle here: https://jsfiddle.net/50oz3xtr/
I've been trying different settings in the following block:
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
But without luck (strength, distanceMin...)
My questions are:
- How can i set a "max length" on links, so the graph doesnt spread too much?
- Is there a way to set a "fixed" length to links?
What you can do is to set a "ideal" length on each link which the simulation will try to optimise to.
To do this, chain the distance method to the end of your "link" force - api reference
e.g.
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance({x}));
You can also pass in a function to the distance method, which allows you to vary ideal distances on a link by link basis.
Also, bear in mind that you have more than one force acting here, so other forces may "interfere" with your link force. I would look at playing with the "strength" setting on each link, and consider using a "collide" force instead of many body.
Playing with the parameters to each force should get you the result you are after.
Related
I’m having issues with D3’s excellent quadtree appearing to drop nodes unpredictably. I can understand that it might not return all nodes if they are closely overlapping, but it would be very useful to understand more about when this might happen so I can work around it.
But that assumes that I’m not misusing it. If I run this with 10,000 points in data below, I get about a consistent ~29% drop in leaf nodes. With only 200 I can get one drop. This feels too high.
Am I doing something wrong with my quadtree implementation?
What could I do to work round this?
var quadtree = d3.geom.quadtree()
.x(function(d){return d[0];})
.y(function(d){return d[1];});
var data = d3.range(10000)
.map(function(d){
return [
Math.random(),
Math.random()
];
});
If I run this count of quadtree leaves, I get a number below data.length:
var qt = quadtree(data),
count = 0;
qt.visit(function(p,x1,y1,x2,y2){
if(p.leaf)count++;
});
But if I run this filter, it returns an empty array suggesting that they are all there:
data.filter(function(d){return qt.find([d.x,d.y]).id !== d.id;});
Where am I going wrong?!
Leaf and point are not interchangeable. Points can exist on internal nodes.
https://github.com/mbostock/d3/wiki/Quadtree-Geom
In a previous post called "D3: How to create slow transition of Circles for nodes in Force Directed Graphs FDG?", I got a great answer for how to transition a single element (e.g. the radius for "just circles") in D3.
My followup question is now about how to transition "multiple D3 attributes" at the same time...
As a reminder, I'm using D3 generated Radio Buttons to toggle the size of Nodes in a FDG Layout (on mouse click) from a default size to a scaled magnitude. You can find the Radio Buttons in the upper left hand of the Node Cluster Diagram (http://nounz.if4it.com/Nouns/Applications/A__Application_1.NodeCluster.html)
The code that toggles the node circles between a default number and a scaled magnitude (now using transitions) looks as follows...
var densityControlClick = function() {
var thisObject = d3.select(this);
var typeValue = thisObject.attr("density_type");
var oppositeTypeValue = (function() {
if(typeValue=="On") {
return "Off";
} else {
return "On";
}
})();
var densityBulletSelector = "." + "densityControlBullet-" + typeValue;
var selectedBullet = d3.selectAll(densityBulletSelector);
selectedBullet.style("fill", "Black")
var oppositeDensityBulletSelector = "." + "densityControlBullet-" + oppositeTypeValue;
var selectedOppositeBullet = d3.selectAll(oppositeDensityBulletSelector);
selectedOppositeBullet.style("fill", "White")
if(typeValue=="On") {
var selectedNodeCircles = d3.selectAll("#NODE");
selectedNodeCircles.transition().duration(500).attr("r", function(d){ return rRange(d.rSize); });
}
else {
var selectedNodeCircles = d3.selectAll("#NODE"); selectedNodeCircles.transition().duration(500).attr("r", function(d) { if (d.id==focalNodeID) { return centerNodeSize; } else { return defaultNodeSize; } } );
}
}
Everything works great and you can see the slower node transitions when you select the radio buttons. However, I'd now like to learn how to transition multiple elements, such as the the radius and the edge lengths simultaneously, along with the theory behind doing so, in order to show off D3's dynamic nature.
My question is: Given that I already can successfully transition the radius of circles, how would I also transition other elements like the edge lengths based on attributes like "alpha", "friction", etc., and... what's the theory behind transitioning multiple elements (in other words, what does the code mean, in English)? The D3 API doesn't appear to clearly get into the theory behind transitioning multiple attributes, simultaneously.
So transitioning multiple attributes is the simple part of this question. Just like a regular selection you can set multiple attributes at a time on your transition:
selectedNodeCircles.transition().duration(500)
.attr("r", function(d){ return rRange(d.rSize); })
.attr("stroke", 'red');
This will transition your radius and your line colour. The transition is a property of the DOM element (in this case the circle) and it will transition as many DOM attributes as you like. The thing to remember is that there is only only one transition object on each DOM element. So if you create another you will overwrite the old one.
// This will NOT work
circles.transition().duration(1000).attr('r', 50);
// The radius transition will be overridden by the fill
// transition and so will not complete
circles.transition().duration(1000).attr('fill', 'red');
This can actually be quite useful because you don't have to worry about interrupting animations that are in progress and figure out how far along they are and then starting a new animation - this will generally be handled automatically.
In your case you want to transition edge lengths in your graph. These are determined by the positional attributes of the nodes. Judging by your finished product, these attributes are already being animated because you are updating the DOM on every iteration of the layout algorithm (not through transitions) probably in the tick() callback.
So you could use transitions inside your tick callback, which might look odd and may be a hassle to keep in synch with the radius transitions (you will have to set both attributes in the transition). But it might be just what you need.
Alternatively, if you can wait, don't update the DOM in the tick callback. Let the layout complete - it runs a lot faster when it is not rendering on each tick - and once it is complete you can animate the radius and x and y attributes to their final positions. Of course this means you'll want good starting positions.
I've been able to build a Force Directed Graph using a Force Layout. Most features work great but the one big issue I'm having is that, on starting the layout, it bounces all over the page (in and out of the canvas boundary) before settling to its location on the canvas.
I've tried using alpha to control it but it doesn't seem to work:
// Create a force layout and bind Nodes and Links
var force = d3.layout.force()
.charge(-1000)
.nodes(nodeSet)
.links(linkSet)
.size([width/8, height/10])
.linkDistance( function(d) { if (width < height) { return width*1/3; } else { return height*1/3 } } ) // Controls edge length
.on("tick", tick)
.alpha(-5) // <---------------- HERE
.start();
Does anyone know how to properly control the entry of a Force Layout into its SVG canvas?
I wouldn't mind the graph floating in and settling slowly but the insane bounce of the entire graph isn't appealing, at all.
BTW, the Force Directed Graph example can be found at: http://bl.ocks.org/Guerino1/2879486enter link description here
Thanks for any help you can offer!
The nodes are initialized with a random position. From the documentation: "If you do not initialize the positions manually, the force layout will initialize them randomly, resulting in somewhat unpredictable behavior." You can see it in the source code:
// initialize node position based on first neighbor
function position(dimension, size) {
...
return Math.random() * size;
They will be inside the canvas boundary, but they can be pushed outside by the force. You have many solutions:
The nodes can be constrained inside the canvas: http://bl.ocks.org/mbostock/1129492
Try more charge strength and shorter links, or more friction, so the nodes will tend to bounce less
You can run the simulation without animating the nodes, only showing the end result http://bl.ocks.org/mbostock/1667139
You can initialize the nodes position https://github.com/mbostock/d3/wiki/Force-Layout#wiki-nodes (but if you place them all on the center, the repulsion will be huge and the graph will explode still more):
.
var n = nodes.length; nodes.forEach(function(d, i) {
d.x = d.y = width / n * i; });
I have been thinking about this problem too and this is the solution I came up with. I used nodejs to run the force layout tick offline and save the resulting nodes data to a json file.
I used that as the new json file for the layout. I'm not really sure it works better to be honest. I would like hear about any solutions you find.
I am trying to get a tree layout for a data set in which i have nodes which can have multiple parents. The links that are created in such instances have extremely large path values. is there anyway to overcome this. I can create a force layout but the tree layout feels more structured and natural to the problem in hand.
Any help is much appreciated.
If you've got multiple parents, you don't really have a tree structure. A network layout is your best option. Here is an example of a a force layout with constraints. In that case I've forced the x-values to specific values. You could do something similar using the tree level to set your x or y value.
The relevant code is:
var adjust_x = function(d) {
return x(d.time);
};
force.on("tick", function () {
link.attr("x1", function (d) {
return adjust_x(d.source);
}) // snipped remainder...
I have a force layout with potentially a very large number of nodes, too large for the graph to render responsively. I was thinking that one way to improve the performance of the system was to prune the graph by eliminating nodes based on in- and out-degree when the number of nodes gets too large.
Recomputing the node and link lists is a bit of a nuisance because links are related to indexes in the node array, and so all the links would need to be re-built.
It seems more elegant to be able to mark individual nodes for exclusion (analogously to the way some nodes are fixed) and have the layout algorithm skip those nodes. This would allow me to dynamically select subsets of the graph to show, while preserving as much state for each node (e.g., position) as practical.
Has anyone implemented something like this?
UPDATE:
I tried to implement the filter suggestion, but ran into an interesting error. It appears that the filter method returns an object that does not implement enter:
qChart apply limit:2
NODES BEF: [Array[218], enter: function, exit: function, select: function, selectAll: function, attr: function…]
NODES AFT: [Array[210], select: function, selectAll: function, attr: function, classed: function, style: function…]
Uncaught TypeError: Object [object Array] has no method 'enter'
The following code is run to get from BEF to AFT:
nodeSubset = nodeSubset.filter(function(n) { return (n.sentCount() <= limit); });
UPDATE 2:
I created a jsfiddle to isolate the problem. This example implements my interpretation of ChrisJamesC's answer. When I tried to implement his suggestion directly (putting the filter after the data), the subsequent call to enter failed because the object returned by filter did not have enter defined.
The goal is to make the layout select only those nodes that have active == true, so in this example, this means that node b should be excluded.
You can use the selection.filter() option combined with the node.weight attribute.
What you would normally do is:
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
Here you can do:
var node = svg.selectAll(".node")
.data(graph.nodes)
.filter(function(d){return d.weight>3})
.enter();
You might also have to remove from drawing the links going to these nodes using the same method.
EDIT You should just filter the data you provide if you want to mark nodes as active directly in the data array (and do the same for links)
var node = svg.selectAll(".node")
.data(force.nodes().filter(function(d) { return d.active; }));
var link = svg.selectAll(".link")
.data(force.links().filter(function(d) {
var show = d.source.active && d.target.active;
if (show)
console.log("kept", d);
else
console.log("excluded", d);
return show;
}) );
Fiddle
If you want to do this by computing the weight of each node, I would still recommend you to do this before passing the nodes and links to the graph and mark nodes as active or not following a specific criteria, then filter the links according to active nodes. Otherwise you would have to load the whole force directed layout only to get the weight to then filter the data to re-load the force directed graph.