Appending elements to d3 selection - javascript

In a d3 program, I have a many circle elements with data bound to them. In a callback to an event, I want to filter out most of them and place a larger red circle on top of those that are left. It looks something like this:
svg.selectAll("circle")
.filter(/* filter function not important */)
.append("circle")
.attr("class", "extra")
.attr("cx", function(){return d3.select(this).attr("cx")})
.attr("cy", function(){return d3.select(this).attr("cy")})
.attr("r", 5);
And the result is this (the first line is the original circle):
<circle cx="55.41208075590415" cy="279.3650793650794" r="1">
<circle class="extra" r="5"></circle>
</circle>
So I have two problems. One, it's inserting as a child of the old element, rather than a sibling. Ideally it would be inserted as the last child of svg. Second, cx and cy aren't getting copied over. What magic do I have to utter to make this work?
If you are okay overwriting existing elements, see the answer below. To create new ones, here's how.
svg.selectAll("circle")
.filter(/* filter function not important */)
.each(function(d){
svg.append("circle")
.attr("class", "extra")
.attr("cx", function() { return x(d.fop) })
.attr("cy", function() { return y(d.bar) })
.attr("r", 5);
})
Notice two things. One, the inner functions do not take parameters, because there is no data bound to the extra circle. Use the d from the existing circle instead. Two, I couldn't figure out how to access the cx and cy of the existing circle, so I had to recompute them instead, which means the scales x and y must remain unchanged. I'll change the accepted answer if anyone can improve on this.

Instead of doing a .append on your selection you should simply use your filter function to modify the circles whose data have the criterion that pass your filter function. You can modify those circles, instead of appending a new element. So your code would look something like this.
svg.selectAll('circle')
.filter(/** what you want**/)
.attr('r', newRadiusSize)
.attr('fill', 'red')
You could even add a nice transition to make the circles change to red instead of just snapping to the newer bigger circles.

Related

How to add and remove nodes with nested groups inside a D3 force layout?

I have a D3 force layout that updates the number of nodes several times per second and that is called by restartD3(). I currently have a circle appended to each node and that works great. However, I now need to have 2 circles per node, but the key here is that they need to be on unique layers by type not by node, so I need to put them in groups. Here is the grouping layering that I am talking about: link
I want this.circleNode to become an outer group for 2 other groups inside so that I only have to manipulate x and y position for the outer group and the 2 inner groups move as well. Here is my current code with just 1 outer group for the nodes but no groups nested inside yet:
// Create force simulation
this.force = d3.forceSimulation(this.users)
.alphaDecay(0)
.velocityDecay(0)
.on('tick', this.tickActions);
// Create circle nodes
this.circleNode = this.d3Graph.selectAll(null)
.enter()
.append("g")
// Call our restartD3 function
this.restartD3();
// My restartD3 function
restartD3() {
// Circles
this.circleNode = this.circleNode.data(this.users, function(d) {
return d.id;
});
this.circleNode.exit().remove();
this.circleNode = this.circleNode
.enter()
.append("circle")
.attr("class", "usercircles")
.attr("r", this.userRadius)
.attr("fill", d => "#00aced")
.merge(this.circleNode)
this.force.nodes(this.users);
}
I haven't been able to implement this, and my attempts have been far off with nothing rendering. Any guidance on nesting groups in a node that works with updating a lot would be much appreciated.

D3 update circle-pack data new nodes overlap existing nodes

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.

Unable to correctly redraw in d3.js after removing the first item from the nodes

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();

d3.js How to target and change elements' attributes

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.

Dynamically updating in d3 works for circles but not external SVGs

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.

Categories