I am building something quite similar to this. What I would love is to make every node either their size as defined in the json file, OR, if it has no size attribute but a children attribute in json, the sum of all of its children's sizes. How would one go about doing that? I have tried various methods but short of adding things up and hardcoding it in JSON, which is a bit lame, I haven't found anything that really would have worked ;( Any suggestions, hive mind?
If your data is a tree structure, you could use a Partition Layout to initialize positions and sizes of nodes. The d.value returned by partition for parent nodes is by default the sum of values for all children nodes, assuming you've properly set the value accessor function to return the data variable that you want to use for size for leaf nodes.
Although the standard display in partition examples is to have space-filling rectangles or arcs instead of nodes and links, it still has all the basic functionality of the other hierarchy layouts. So once you've run the layout on your root to generate your array of nodes, you can run the links function to calculate the links.
If you still want a force-based layout instead of a static tree, you can just pass in your nodes and links to the force layout and start it up.
In sum:
var root = /*root object read from JSON data*/;
var w,h; /*set appropriately */
var partition = d3.layout.partition()
.size([w,h])
.value(function(d){return d.size;})
.children(function(d){return d.children;})
//optional: this is the default, change as needed
.sort(null);
//optional: turns off sorting,
//the default is to sort children by descending size
var nodes = partition(root);
var links = partition.links(nodes);
initialize();
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([w,h])
/*and any other customization*/
.start();
svg.on("tick", update);
One thing to note. The x value created by partition layout is designed to be the corner of a rectangle instead of the centre of a circle. So if you position your nodes based on the original x value, you'll end up with parents off to the far left of their children. If you're running everything through a force-based layout afterwards, it will sort itself out eventually, but you can centre them from the beginning by setting d.x = d.x + d.dx/2 and d.y = d.y + d.dy/2 on all your nodes during initialization (e.g., using an .each() call in your enter() chain). And of course, use d.value to initialize your node size (with an appropriate scale).
Related
Posting both question & answer here to save somebody else the same trouble later...
When I create two sunburst charts using d3.layout.partition, the first sunburst's slice proportions are overwritten by the second sunburst's slice proportions upon resize of the slices.
The two charts pass different .value accessor functions into the partition layout, e.g.
d3.layout.partition()
.sort(null)
.value(function(d) { return 1; });
vs.
d3.layout.partition()
.sort(null)
.value(function(d) { return d.size; });
And they generate their own list of nodes that are not shared between the two sunbursts. However, if I re-call the d3.svg.arc generator to resize to larger radius (but not change overall proportions), the slice angles are suddenly overwritten.
See the example here: http://bl.ocks.org/explunit/ab8cf15534f7fec5ac6d
The problem is that while partition.nodes() seems to generate a new data structure (e.g if you give it some .key functions, it writes the extra properties (e.g. .x, .y, .dx, dy) to the underlying data and does not make a copy of the data. Thus if the data structure is shared between the two charts, these .x, .y, .dx, dy properties will bleed through to the other graphs.
This seems like a bug to me, but in reading this old GitHub issue it seems to be treated as "by design". Perhaps it will be reconsidered in future versions.
One workaround is to use something like Lodash/Underscore cloneDeep or Angular's copy to make each chart have it's own copy of the data.
makeSunburst(null, _.cloneDeep(root), countAccessorFn);
makeSunburst(null, _.cloneDeep(root), sizeAccessorFn);
See example here: http://bl.ocks.org/explunit/e9efb830439247eea1be
An alternative to copying the whole dataset for each chart would be to simply recompute the partition before re-rendering.
Instead of having makeSunburst() be a function of the accessor, make it a function of the partition. Pass a different partition function to each chart:
// create separate partition variables
var countPartition = d3.layout.partition().sort(null).value(countAccessorFn);
var sizePartition = d3.layout.partition().sort(null).value(sizeAccessorFn);
// make the charts as a function of partition
charts.push(makeSunburst(root, countPartition));
charts.push(makeSunburst(root, sizePartition));
Then before applying the transition, simply update the nodes variable to reflect the associated partition:
addToRadius: function(radiusChange) {
radius += radiusChange;
ringRadiusScale.range([0, radius]);
// update the data before re-rendering each chart
nodes = partition.nodes(dataRoot);
path.transition().attr('d', arc);
}
Now when you update each chart, it is using the correct partition.
Here's an updated example.
Is there a better idiom to append a <svg:g> grouped set of elements to a container on an enter() selection as part of a generic update pattern?
var cell = d3.select(this); // parent container
cell = cell
.selectAll('.plot').data([0]); // seems kludgy
cell
.enter().append('g').classed('plot',true); // append the wrapping <g> element
cell = cell
.selectAll("circle")
.data(_.values(slice));
cell
.enter().append("circle"); // general enter() -- create plot elements
cell.attr() // etc. general update--style plot elements
cell
.exit().remove();
Of course,
if ( cell.select('g.plot').empty() ) {
cell = cell.append('g').classed('plot', true);
}
instead of the first two statements would do it too, but this seems like a very common operation and the selectAll().data([0]) seems contrived--is there a more elegant d3 idiom?
For creating an element if necessary or selecting it otherwise, I would usually use a structure similar to your if block as opposed to using a data join with meaningless data.
Not only is it shorter code, but it means that you're not carrying around that extra data property on your element when it doesn't have any significance. It's also easier for other people to figure out what you're doing!
The only thing I would change is to actually save the selection that you're using for the .empty() test, since if it's not empty you'll be using it. (You could use another variable to save the this-selection, but d3.select(this) isn't exactly a high computation method call to repeat, and even then you'll only be repeating it once, when you first create the group.)
var plot = d3.select(this) // this is the parent container
.selectAll('g.plot'); //select the plot group if it exists
if ( plot.empty() )
plot = d3.select(this).append('g').classed('plot',true);
//create the plot group if necessary
var cell = plot.selectAll("circle") //now select/create data elements as usual
.data(_.values(slice));
cell
.enter().append("circle"); // general enter() -- create plot elements
cell.attr() // etc. general update--style plot elements
cell
.exit().remove();
Simply append a "g" for every new group of elements that you need.
var cell = d3.select(this)
.append("g")
.attr("class","plot")
.selectAll("circle")
.data(…);
cell.enter().append("circle")
.attr(…);
cell.exit().remove();
What doesn't work here?
This is a continuation of my efforts to build a collapsible tree layout using d3.js.
Generate (multilevel) flare.json data format from flat json
The layout looks like: (http://bl.ocks.org/mbostock/raw/4339083/) with around 3k nodes and depth of some nodes around 25. The current size of the canvas I need to set is 8000px width and 8000px height in order that all nodes are visible which I know is not reasonable when the number of tree levels rendered is 2 or 3.
Furthermore, I intend to make this code reusable with other trees that maybe smaller/larger in size based on what data source(json file) is selected.
So I was wondering if it is possible to resize the canvas size relative to the positions of the nodes/ number of nodes shown on screen. This way, the code would be much more systematic and adaptable.
I saw this:
Dynamically resize the d3 tree layout based on number of childnodes
but this resizes the tree, which if you can imagine in a case of tree with around 3k nodes, makes it hard to read and comprehend.
I know this might not even be related to d3.js but I tagged it to explain my issue and bring in d3 experts too who might have faced a similar condition.
I am also attempting to filter out uninformative nodes based on my criteria so as to render less number of nodes than the actual data. (I know i will run into performance issues with larger trees). Any help would be much appreciated.
NOTE: When I say canvas, I mean the area on which the tree is drawn and not the "canvas". I am not familiar with the jargon so kindly read accordingly.
Hope this helps someone.
I faced similar problems also using the flare tree code as a base for what I was building and the suggested links did not seem to account for a lot of variance in node structure? I have many trees to display with a dynamic number of nodes and structuring. This solution worked for me:
Concerning height: After observing the translate offsets per node, I learned that d.x (vs d.y, as tree is flipped) is an offset from the root node, with the nodes above root going negative and those below going positive. So, I "calculated" the max offset in both directions each time a node is appended, then with that information, adjusted the canvas height and the view translation (starting point of root).
For width: max d.depth * by the (y length normalization the code uses) + margins
let aboveBount = 0
let belowBound = 0
let maxDepth = 0
nodeEnter.each( (d) => {
if( Math.sign(d.x) === -1 && d.x < boundAbove) {
boundAbove = d.x
}
if( Math.sign(d.x) === 1 && d.x > boundBelow) {
boundBelow = d.x
}
if( d.depth > maxDepth){
maxDepth = d.depth
}
})
const newHeight = boundBelow + Math.abs(boundAbove) + nodeHeight*2 + margin.top*2
svg.style('height', newHeight)
svg.style('width'. maxDepth*180 + margin.left*2)
//180 was the amount set to normailze path length
svg.attr('transform', `translate(${margin.left}, ${Math.abs(boundAbove) + margin.top})`)
Well, best wishes and happy coding!
I was facing the similar problem and now I have find out a solution. Check on this link. D3 collapsible tree, node merging issue
In this example: http://bl.ocks.org/mbostock/1747543:
...Mike shows us how to avoid collision among nodes so that no two nodes overlap each other.
I wonder if it is possible to avoid collision between nodes and edges so that no node 'clips' or overlaps an edge unless it is connected by that edge.
The following example using D3 force-direct shows that node L overlaps with the edge connecting I and A, and similarly, node M overlaps with the edge connecting L and D. How do we prevent such cases?
If your graph doesn't have too many nodes, you can fake it. Just insert one or more nodes for each link, and set their position along the link in the tick handler. Check out http://bl.ocks.org/couchand/7190660 for an example, but the changes to Mike Bostock's version amount to basically just:
var linkNodes = [];
graph.links.forEach(function(link) {
linkNodes.push({
source: graph.nodes[link.source],
target: graph.nodes[link.target]
});
});
and
// force.on('tick', function() {
linkNodes.forEach(function(node) {
node.x = (node.source.x + node.target.x) * 0.5;
node.y = (node.source.y + node.target.y) * 0.5;
});
This will introduce a pretty serious performance overhead if you have very many nodes and edges, but if your graph doesn't get much larger than your example it would hardly be noticed.
You may also want to fiddle with the relative force of the real nodes versus the link nodes.
Take this one step further and you get the nice curved links of http://bl.ocks.org/mbostock/4600693.
I am loading json from database and creating a json file which loads fine. Now I don't know which steps to take for making the nodes responsive in a Force-Directed Graph. I need to remove and add new nodes and their links.
force.nodes(json.nodes)
.links(json.links)
.start();
initNodes(json);
How can I make this more dynamic or update it without resetting the whole visualization?
I have seen this question a couple of times not being answered so I hope someone can post and give a guide.
Adding nodes/links to my D3 force graph was very confusing until I better understood the way I was adding the initial set of nodes.
Assuming a <g> is what you'd like to use for your nodes:
// Select the element where you'd like to create the force layout
var el = d3.select("#svg");
// This should not select anything
el.selectAll("g")
// Because it's being compared to the data in force.nodes()
.data(force.nodes())
// Calling `.enter()` below returns the difference in nodes between
// the current selection and force.nodes(). At this point, there are
// no nodes in the selection, so `.enter()` should return
// all of the nodes in force.nodes()
.enter()
// Create the nodes
.append("g")
.attr("id", d.name)
.classed("manipulateYourNewNode", true);
Now let's make that function that will add a node to the layout once the graph has been initialized!
newNodeData is an object with the data you'd like to use for your new node.
connectToMe is a string containing the unique id of a node you'd like to connect your new node to.
function createNode (newNodeData, connectToMe) {
force.nodes().push(newNodeData);
el.selectAll("g")
.data(force.nodes(), function(datum, index) { return index })
The function given as the optional second argument in .data() is run once for each node in the selection and again for each node in force.nodes(), matching them up based on the returned value. If no function is supplied, a fallback function is invoked, which returns the index (as above).
However, there's most likely going to be a dispute between the index of your new selection (I believe the order is random) and the order in force.nodes(). Instead you'll most likely need the function to return a property that is unique to each node.
This time, .enter() will only return the node you're trying to add as newData because no key was found for it by the second argument of .data().
.enter()
.insert("g", "#svg")
.attr("id", d.name)
.classed("manipulatYourNewNode", true);
createLink(connectToMe, newNodeData.name);
force.start();
}
The function createLink (defined below) creates a link between your new node and your node of choice.
Additionally, the d3js API states that force.start() should be called after updating the layout.
Note: Calling force.stop() at the very beginning of my function was a huge help for me when I was first trying to figure out how to add nodes and links to my graph.
function createLink (from, to) {
var source = d3.select( "g#" + from ).datum(),
target = d3.select( "g#" + to ).datum(),
newLink = {
source: source,
target: target,
value: 1
};
force.links().push(newLink);
The code below works under the assumptions that:
#links is the wrapper element that contains all of your link elements
Your links are represented as <line> elements:
d3.select("#links")
.selectAll("line")
.data(force.links())
.enter()
.append("line");
You can see an example of how to append new nodes and relationships here:
http://bl.ocks.org/2432083
Getting rid of nodes and relationships is slightly trickier, but you can see the process here:
http://bl.ocks.org/1095795