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.
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 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.
I'm learning D3, and I came across an issue while creating a chart using D3 and SVG.
The full code (but with a subset of the real data) can be found here:
http://jsbin.com/lemazefa/1/edit.
A simplified example can be found here: http://jsfiddle.net/antoinejaussoin/G7zms/
(This is a better one, since it removes all the unecessary code around the problem).
.on('click', function(d, i){
console.log('I have clicked on '+d);
data.splice(i, 1);
console.log('Data is now: '+data);
update(data);
})
My problem is the following: when I click on one of the circle on the chart, I want it removed.
As far as I can see, on the first click, the right item is returned by the click event, then properly removed from the data array, but when I rebind the chart with the new data, the wrong circle is removed. And from that moment, the wrong data is associated with all the other circles.
Any idea why that could be? Is that a D3 bug? Or more likely, am I doing something wrong?
Many Thanks!
You are not updating the attributes of the DOM elements that remain after the removals. This is because you are setting the attributes off of the enter selection, which is always empty after the initial append.
So, change the code to:
function update(data) {
var circle = svg.selectAll("circle")
.data(data);
// enter selection
circle.enter()
.append("circle")
.on('click', function (d, i) {
console.log('I have clicked on ' + d);
data.splice(i, 1);
console.log('Data is now: ' + data);
update(data);
})
// update selection
circle.attr("fill", "steelblue")
.attr("cy", 90)
.attr("cx", function (d) {
return d;
})
.attr("r", Math.sqrt);
// exit selection
circle.exit().remove();
}
Updated FIDDLE.
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();
I'm creating a bunch of Objects (using a pseudo-class, so they all have the same structure) with the properties "name", "type", "status", "poxitionX" and "positionY".
Then I activate my SVG drawing area using d3.js
var svg = d3.select("#d3canvas")
.append("svg")
.attr("width", 600)
.attr("height", 400);
and draw my Objects as circles on the canvas
function drawCircle (objectHandle) {
var tempDrawVar = svg.append("circle")
.style("stroke", "white")
.style("fill", "orange")
.attr("r", 20)
.attr("cx", objectHandle.positionX)
.attr("cy", objectHandle.positionY)
.on("click", function(d){
objectHandle.doStuff();
});
}
doStuff() is a method / prototype function that is supposed to ask the user for input and react to the user input by changing attributes of some of the previously created circles.
The problem is, that I don't know how to "target" those circles. I can update the properties in the Objects just fine, but I really don't think completely deleting the canvas area and creating new circles with every "update" is anywhere near a decent solution.
I can't use tempDrawVar outside the function, and even if I could it would be overwritten each time a new circle is draw anyway (or is it? I'm not sure, I admit).
I tried creating an global Array, using the draw function's parameter as index and using that as the variable instead of tempDrawVar. The drawing function works, but the array stays empty...
var circleArray = new Array();
function drawCircle (objectHandle) {
circleArray[objectHandle] = svg.append("circle")
...
Can anybody point me in the right direction? (In a nutshell: How can I create a function that targets a specific "item" created with d3.js and change one or more of its attributes?)
There are a few options for identifying specific elements. You can use a DOM selector, which means that you would need something like an ID or a class attached to the element to target it.
d3.selectAll("circle").data(data).enter()
.append("circle")
.attr("id", function(d) { return d.id; })
.attr("class", function(d) { return d.class; });
// select DOM element for first data element based on ID
d3.select("#" + data[0].id);
// select by class
d3.select("circle." + data[0].class);
Alternatively, you can use D3's data binding to do the matching. This relies on having a matching function that tells D3 how to match data to DOM elements.
// second argument to .data() is the matching function
d3.selectAll("circle")
.data(data, function(d) { return d.id; })
.enter()
.append("circle");
// select the DOM element for the first data element
d3.selectAll("circle")
.data([data[0]], function(d) { return d.id; });
The latter is probably the more D3 way to do it, but more code (and not particularly nice code at that) to select the element. You also have to remember to pass the same matching function to .data() all the time.
In the example code you've posted, it doesn't look as if you're binding any data to the created DOM elements, so you would have to assign an ID or a class to be able to identify them.