I'm following the General Update Pattern but having an issue with regards to layering.
Using a circle-pack layout, I pack the new data, update, enter and exit the circle elements. However, when new elements enter, they overlap the updated circles.
Data key function is based on element name:
.data(nodes, function(d, i) { return d.name; });
So my circle pack has a spot for the updated circle (of the correct location and size) but it's hidden behind its newly entered parent circle.
Is there a way to send these updated nodes to the front or redraw them over the entered circles?
--UPDATE--
As suggested by the person who closed this issue, I've tried implementing the linked to solution using moveToFront.
I added the following code in my update section (which didn't change anything) and then tried adding it after the enter and exit code, which also didn't make any difference.
.each("end", function(d){ d3.select(this).moveToFront(); });
d3.selection.prototype.moveToFront = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};
For clarity, this is what the selection and update looks like:
// Load data into svg, join new data with old elements, if any.
var nodes = pack.nodes(postData);
node = root = postData;
groupNodes = svg.selectAll("g")
.data(nodes, function(d, i) { return d.name; });
// Update and transition existing elements
groupNodes.select("circle")
.transition()
.duration(duration)
.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; })
.attr('r', function(d) { return d.r; })
.each("end", function(d){ d3.select(this).moveToFront(); });
This moveToFront code does not make a difference to my output, and the updated circles remain behind the entered selection circles.
To summarize: the issue seems to be caused by a hierarchy layout (circle-packing) which expects the circles to be drawn in the order of the data's hierarchy. The d3 update pattern (using enter, update and exit selections) causes selected update elements to remain in the svg when the hierarchy is re-drawn, and the new layers are drawn over it. The parents of those nodes are already correctly set, so parentNode.appendChild doesn't do anything in this case, because it's not the cause of the issue.
Here is a fiddle to demonstrate my issue. I've tried putting the moveToFront code in various places, with no visible difference.
When you hit the "Change Data" button, it'll redraw the circles, but any circles whose names overlap between the two data sets are not nested properly in the circle-pack. Children of "Group A" are hidden behind one of the parent circles. You can verify the nodes are there via Inspect Element.
Another pic from the updated fiddle:
D3 provides a way to reorder elements based on the data bound to them with the .sort() function. In your case, the condition to check is the .depth attribute of the elements -- "deeper" elements should appear in front:
svg.selectAll("g")
.sort(function (a, b) {
if (a.depth < b.depth) return -1;
else return 1;
});
Complete demo here.
Related
Could I add a level selector for these trees created with d3.js?
http://bl.ocks.org/mbostock/2966094
or
bl.ocks.org/mbostock/4339083
Add a label on each level to get the level position or expand it.
Added a example picture.
Taking the example here: http://bl.ocks.org/mbostock/4339083
I would start by nesting the nodes by level:
var nodesByLevel = d3.nest().key(function (d) {return d.depth}).entries(nodes);
To add your boxes, do something like:
svg.selectAll(".levelBox")
.data(nodesByLevel)
.enter() // one box per level
.append("text")
.attr("class","levelBox")
.attr("x", function (d) {return d.values[0].x}) //take the x of the first node in this layer
.text(function(d) {return d.key}) //the key from the nesting, i.e. the depth
.onclick(levelExpand); // click handler
The above is just a skeleton, that should go into the update function (you need to take care of the exit() and update() selections after adding the data, and any additional drawing features).
In levelExpand, you have access to the list of nodes for the box that is clicked (in d.values). You can then go through the list, expand them, and then update the drawing
function levelExpand(d) {
d.values.forEach(function (n) {n.children = n._children;}); //expand all nodes internally
update(root); //show the update
}
I'm trying out a way to get paths to display next to each other, such that they'll push each other around (factoring in widths and neighbouring points) and not overlap.
This is my fiddle, mostly pieced together from examples
https://jsfiddle.net/crimsonbinome22/k2xqn24x/
var LineGroup = svg.append("g")
.attr("class","line");
var line = d3.svg.line()
.interpolate("linear")
.x(function(d) { return (d.x); })
.y(function(d) { return (d.y); })
;
LineGroup.selectAll(".line")
.data(series)
.enter().append("path")
.attr("class", "line")
.attr("d", function(d){ return line(d.p); })
.attr("stroke", function(d){ return d.c; })
.attr("stroke-width", function(d){ return d.w; })
.attr("fill", "none");
And this is what I'm hoping to achieve in this image here, basically:
For all lines landing on the same point, push them left or right of that point so together they center around it.
Factor in line width so they don't overlap, or leave whitespace between.
Be able to handle paths with different numbers of points (max in example is 3 but I want to deal with up to 10)
Note though points that overlap will always have the same index (they won't loop around, but just go outwards like a tree)
Be able to handle different numbers of lines landing on the same point.
Some issues I'm having:
I'm new to d3 and I find functions a bit baffling. Not sure how to even start to apply logic that will move the lines around.
My data structure has some redundant info in it, such as r for the rank (to decide whether to push left or right) and w for the width both of which will always be the same for a particular line.
I have a lot of data so the data structure used here won't work with the csv data I have. Can maybe skip this one for now and I'll open up a new question for that one later.
I've had a search around but can't find any examples of how to do this. In a way it's almost like a chord diagram but a little different, and I can't find much relevant code to reuse. Any help on how to achieve this (either with the approach I've started, or something totally different if I've missed it) would be appreciated.
I would go with the following steps:
compute an array of node objects, i.e. one object for each point visited by a line
compute the tree on this node (that is, for every node, add links to its parent and children)
make sure that children of any node are ordered according to the angle they make with this node
at this point, each line now only depends on its final node
for each node compute an ordered list of lines going through
visit all nodes bottom-up (i.e. starting from the leaves)
the "go-through" list is the concatenation of the lists of the children + all lines that end at the current node
for each node, compute an array of offsets (by summing the successive width
of the lines going through)
finally, for every line and every node in the line, check the array of offsets to know how much the line must be shifted
Edit: running example
https://jsfiddle.net/toh7d9tq/1/
I have used a slightly different approach for the last two steps (computing the offset): I actually create a new p array for each series with a list of pairs {node, offset}. This way it is much easier to access all relevant data in the drawing function.
I needed to add an artificial root to have a nice starting line (and to make it easier for recursion and angles and everything), you can skip it in the drawing phase if you want.
function key(p) {
return p.time+"_"+p.value
}
// a node has fields:
// - time/value (coordinates)
// - series (set of series going through)
// - parent/children (tree structure)
// - direction: angle of the arc coming from the parent
//artificial root
var root={time:200, value:height, series:[], direction:-Math.PI/2};
//set of nodes
var nodes = d3.map([root], key);
//create nodes, link each series to the corresponding leaf
series.forEach(function(s){
s.pWithOffset=[]; //this will be filled later on
var parent=root;
s.p.forEach(function(d) {
var n=nodes.get(key(d));
if (!n) {
//create node at given coordinates if does not exist
n={time:d.time,
value:d.value,
parent:parent,
series:[],
direction:Math.atan2(d.value-parent.value, d.time-parent.time)};
nodes.set(key(n),n);
//add node to the parent's children
if (!parent.children) parent.children=[];
parent.children.push(n);
}
//this node is the parent of the next one
parent=n;
})
//last node is the leaf of this series
s.leafNode=parent;
parent.series.push(s);
})
//sort children by direction
nodes.values().forEach(function(n){
if (n.children)
n.children.sort(function (a,b){
if (a.direction>n.direction)
return a.direction-b.direction;
});
});
//recursively list all series through each node (bottom-up)
function listSeries(n) {
if (!n.children) return;
n.children.forEach(listSeries);
n.series=d3.merge(n.children.map(function(c){return c.series}));
}
listSeries(root);
//compute offsets for each series in each node, and add them as a list to the corresponding series
//in a first time, this is not centered
function listOffsets(n) {
var offset=0;
n.series.forEach(function(s){
s.pWithOffset.push( {node:n, offset:offset+s.w/2})
offset+=s.w;
})
n.totalOffset=offset;
if (n.children)
n.children.forEach(listOffsets);
}
listOffsets(root);
And then in the drawing section:
var line = d3.svg.line()
.interpolate("linear")
.x(function(d) { return (d.node.time-Math.sin(d.node.direction)*(d.offset-d.node.totalOffset/2)); })
.y(function(d) { return (d.node.value+Math.cos(d.node.direction)*(d.offset-d.node.totalOffset/2)); })
;
LineGroup.selectAll(".line")
.data(series)
.enter().append("path")
.attr("class", "line")
.attr("d", function(d){ return line(d.pWithOffset); })
.attr("stroke", function(d){ return d.c; })
.attr("stroke-width", function(d){ return d.w; })
.attr("fill", "none");
I'm trying to remove a circle and its subnodes from a pack layout and recalculate the others. I'm using exit to handle removed data, but whenever I add or remove an element and apply the transition, its context becomes messy:
I can see that before Erlang was a leaf, and later it become the parent of Clipper and Basic (!?). Here is the demo.
I've created a pop function that removes the last element:
window.pop = function() {
data.children.pop();
var selection = svg.datum(data).selectAll(".node").data(pack.nodes);
// Removed nodes
selection
.exit()
.remove();
// Update it all
selection
.transition()
.duration(500)
.attr("transform", function(d) {
return translate(d.x, d.y);
})
.select("circle")
.attr("r", function(d) {
return d.r;
});
};
And also an update function that adds an element.
I imagine that, if the text is having the behavior, it should be because the data got messy, because the text is relatively positioned. The poorly children are being lost from their parents! Why is this happening?
The problem is that D3 doesn't match the right data items with the right elements. You can tell it how to by providing a key function as the second argument to .data():
var selection = svg.datum(data).selectAll(".node")
.data(pack.nodes, function(d) { return d.name; });
Complete demo here.
I'm currently building a d3.js script based on this script. I can get the graph adding new nodes perfectly, but when it comes to removing nodes, it has some trouble.
If I use nodes.pop() to remove the last element, it'll run correctly, but when removing the first element using nodes.shift(), the nodes are redrawn incorrectly. For example, if 4 nodes are added, so that the node array becomes:
[0] = color.orange
[1] = color.blue
[2] = color.green
[3] = color.green
then nodes.shift() is called, the first element is moved correctly, so that the array becomes:
[0] = color.blue
[1] = color.green
[2] = color.green
When being drawn on screen though, node[0], which is now blue, should remain in the same location, but what actually happens is that it moves to where the orange circles are being stored. The Cx and Cy value of the blue circle don't change (the centre for all nodes of that colour) so I'm not really sure what's causing this. If I call removeNode again, the elements will be shifted correctly, but the item now in index 1 will move to where the blue circles are being drawn. I thought that this may be an issue with the node array being used to redraw while the elements were still being shifted, so I used a timeout with a redraw callback, but this didn't work unfortunately. I thought that the id of nodes may need to be decreased to match their index in the array, but this too didn't work.
The script can be found here, although nothing will display at first. Using the console, call addNode(); several times to see how the script runs.
I realised as I was writing this what I had been doing wrong. Following this tutorial showed me that I needed to call circle = circle.data(force.nodes(), function(d) { return d.id;}); at the top of redraw, so that redraw now looks like
var circle = svg.selectAll("circle");
circle = circle.data(force.nodes(), function(d) { return d.id;});
circle
.enter().append("circle")
.attr("cx", function(d) { return d.cx; })
.attr("cy", function(d) { return d.cy; })
.attr("r", function(d) { return d.radius; })
.style("fill", function(d) { return d.color; });
circle.exit().remove();
force.start();
Suppose I want to dynamically update the position and number of circles on a page using d3. I can do this, using the .data(), .enter(), .exit() pattern. Here is a working example.
http://jsfiddle.net/csaid/MFBye/6/
function updatePositions(data) {
var circles = svg.selectAll("circle").data(data);
circles.enter().append("circle");
circles.exit().remove();
circles.attr("r", 6)
.attr("cx", 50)
.attr("cy", function (d) {
return 20 * d
});
}
However, when I try to do the same thing with external SVGs instead of circles, many of the new data points after the first update do not appear on the page. Example:
http://jsfiddle.net/csaid/bmdQz/8/
function updatePositions(data) {
var gs = svg.selectAll("g")
.data(data);
gs.enter().append("g");
gs.exit().remove();
gs.attr("transform", function (d, i) {
return "translate(50," + d * 20 + ")";
})
.each(function (d, i) {
var car = this.appendChild(importedNode.cloneNode(true));
d3.select(car).select("path")
});
}
I suspect this has something to do with the .each() used to append the external SVG objects, but I am at a loss for how to get around this. Also, the "cx" and "cy" attributes are specific for circles, and so I can't think how they could be used for external SVGs.
Thanks in advance!
There are two problems with your code. The first problem, and reason why you're not seeing all the data points, is that your external SVGs contain g elements, which you are selecting. What this means is that after you first appended the elements, any subsequent .selectAll("g") selections will contain elements from those external SVGs. This in turn means that the data you pass to .data() gets matched to those and hence your selections do not contain what you expect. This is easily fixed by adding a class to the g elements you add explicitly and selecting accordingly.
The second problem is that you're executing the code that appends the external SVGs as part of the update selection. This means that those elements get added multiple times -- not something you would notice (as they overlap), but not desirable either. This is easily fixed by moving the call to clone the nodes to the .enter() selection.
Complete jsfiddle here. As for your question about cx and cy, you don't really need them. You can set the position of any elements you append using the transform attribute, as you are doing already in your code.