First element in data getting rerendered rather than updated - javascript

Here is something similar to what is happening in my code : https://codepen.io/anon/pen/zJmvXa?editors=1010
Press the Update button to see the issue.
The problem in this codePen, is its only redrawing the first element (Myriel).
I think it must be around the enter,exit or the merge, but I don't fully grasp what's going on.
I thought the merge was to merge existing data with new, and the only data that should go into the enter should be the new data. And the exit is for removing redundant data?
As this graph has both circles and text, should the merge be made on the 'g' element that contains these ?
Perhaps it's to do with the data function :
.data(dataset1.nodes, function(d, i) {
return d;
});
If i change this to use d.id, it re renders everything again. I presume it's due to the data having an id attribute. How is this suppose to be done ?
// Apply the general update pattern to the nodes.
forceNetwork.node = d3
.select("#nodesContainer")
.selectAll("g.network-node")
.data(dataset1.nodes, function(d, i) {
return d
});
forceNetwork.node.exit().remove();
forceNetwork.nodeEnter = forceNetwork.node
.enter()
.append("g")
.attr("id", function(d, i) {
return "network-g-node-" + d.id;
})
.attr("class", function(d, i) {
return "network-node";
})
.call(
d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
).merge(forceNetwork.node);
forceNetwork.nodeEnter
.append("circle")
.attr("id", function(d) {
return d.id;
})
.attr("class", "networkviewer-node")
.attr("fill", "red")
.attr("r", 20);
// Append images
forceNetwork.nodeEnter
.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.id });

The problem here is that you'er merging the update and enter selections...
forceNetwork.nodeEnter = forceNetwork.node
.enter()
.merge(forceNetwork.node);
... and, after that, appending the circles (and lines):
forceNetwork.nodeEnter
.append("circle")
That will necessarily duplicate the circles and lines.
Instead of that, merge the selection after appending the circles and lines to the enter selection, and also change the selection in your tick function.
Here is the refactored code: https://codepen.io/anon/pen/GXYozm?editors=1010

Related

Filter paths and append text

I would like to set a filter on paths and append text but nothing happens.
var filteredElements = svgContainer.selectAll("path")
//.data(feat.features)
.append("text")
.filter(function (d) {
if (d.properties.myID > 0) {
return true;
};
})
.attr("x", function (d) {
return path.centroid(d)[0];
})
.attr("y", function (d) {
return path.centroid(d)[1];
})
.attr("text-anchor", "middle")
.attr("font-size", "2px")
.text("foo");
filteredElements contains 46 elements which are correct but the text is not being appended.
With that code, it works fine but I need the condition in my filter:
svgContainer.selectAll("path[PE='1442']")
.data(feat.features)
.enter().append("text")
.attr("x", function (d) {
return path.centroid(d)[0];
})
.attr("y", function (d) {
return path.centroid(d)[1];
})
.attr("text-anchor", "middle")
.attr("font-size", "2px")
.text("foo");
I'm adding this as a second answer because there isn't enough room in a comment, but it suffices as an answer itself.
You have paths drawn on the svg, and you want to draw text for a subset of those paths.
There are two approaches that could be used for this. One is to use a parent g element to hold both path and text:
// Append a parent:
var g = svg.selectAll(null) // we want to enter an element for each item in the data array
.data(features)
.enter()
.append("g");
// Append the path
g.append("path")
.attr("d",path)
.attr("fill", function(d) { ... // etc.
// Append the text to a subset of the features:
g.filter(function(d) {
return d.properties.myID > 0; // filter based on the datum
})
.append("text")
.text(function(d) { .... // etc.
The bound data is passed to the children allowing you to filter the parent selection before adding the child text.
The other approach is closer to what you have done already, but you don't quite have idiomatic d3. We also don't need to re-bind the data to the paths (d3.selectAll("path").data(), instead we can use:
svgContainer.selectAll(null)
.data(feat.features.filter(function(d) { return d.properties.myID > 0; }))
.enter()
.append("text")
.attr("x", path.centroid(d)[0])
.attr("y", path.centroid(d)[1])
.attr("text-anchor", "middle")
.attr("font-size", "2px")
.text("foo")
As an aside, your initial approach was problematic in that it:
it appends text to path elements directly, which won't render (as you note)
it is binding data to the paths again, for each element in the selection, you are binding an item of the data array to a selected element - since the selection is a sub-set of your paths, but your data is the full dataset, you are likely assigning different data to each path (without specifying an identifier, the ith item in the full dataset is bound to the ith element in the sub-selection).
I have now a solution, I think. My text nodes were inside my path nodes. Now I'm just doing this in my if condition and add my text node under my paths.
svgContainer.selectAll("path")
.data(feat.features)
.filter(function (d) {
if (d.properties.myID > 0) {
d3.select("svg")
.append("text")
.attr("x", path.centroid(d)[0])
.attr("y", path.centroid(d)[1])
.attr("text-anchor", "middle")
.attr("font-size", "2px")
.text("foo")
};
})

I can not move my nodes, the force diagram does not apply in 3.js

In a previous question a user helped me with a problem that consisted in not knowing how to put images where the circles are. this time my problem is that I can not drag the nodes. This gif illustrates my problem (first I show how the nodes should move normally, then I illustrate my problem.).
Why does this happen?
var link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("stroke", function(d) {
console.log(d);
return colorLink(d.group);
})
.attr("marker-end", "url(#arrow)");
var node = g.selectAll("g.node")
.data(graph.nodes)
//.filter(function(d){ return d.type=="circle"; })
var NodeCircleEnter= node.enter().append("g")
.attr("class","node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
NodeCircleEnter
.append("circle")
.attr("r", 20)
.attr("fill", function(d) {
return colorNode(d.group);
})
.style("stroke", function(d) {
return colorNode(d.group);
})
// Append images
var images = NodeCircleEnter.append("svg:image")
.attr("xlink:href", function(d) {console.log(d); return d.image;})
.attr("x", function(d) { return -25;})
.attr("y", function(d) { return -25;})
.attr("height", 50)
.attr("width", 50);
This is my full code:
https://plnkr.co/edit/9uH13N3or3G9VTgQlMm9?p=preview
Your event handlers need to be applied to the elements themselves rather than the canvas as a whole.
In this code (only existing in your Plunker project, but not in your question):
var drag_handler = d3.drag()
.on("start", drag_start)
.on("drag", drag_drag)
.on("end", drag_end);
drag_handler(node);
Your variable node is the drawing. You actually want a collection of elements to affect which you get from functions like .enter(). Your variable NodeCircleEnter contains that collection so the particular line should be:
drag_handler(NodeCircleEnter);
This still leaves an issue where dragging on the labels themselves doesn't work. This is because the labels aren't children of the elements you set the handlers on.

Why is label not getting added to all my paths?

Plunker: https://next.plnkr.co/edit/17t5ujwC71IK3PCi
Why is following not adding a "test" label to all my polygons?
/* NOT Working code */
groups.selectAll('.path_placeholder')
.enter()
.append('text')
.text("test")
Update
.enter() wasn't required as mentioned by Xavier. Removing it showed "test" for all nodes. But why then its not working when I do provide data and use enter() as following:
groups.selectAll('.path_placeholder')
.data(groupIds, function(d) {
return d;
})
.enter()
.append('text')
.text(function(d){
console.log(d);
return d;
})
I am trying to be able to show label for each of my polygon and for now just trying to add a dummy label to each of them.
Your problem here is the paths is a <path> selection, not a <g> one:
paths = groups.selectAll('.path_placeholder')
.data(groupIds, function(d) { return +d; })
.enter()
.append('g')
.attr('class', 'path_placeholder')
.append('path')//this makes the selection pointing to <path> elements
.attr('stroke', function(d) { return color(d); })
.attr('fill', function(d) { return color(d); })
.attr('opacity', 0);
Because of that, when you do...
groups.selectAll('.path_placeholder')
.data(groupIds, function(d) {
return d;
})
.enter()
//etc...
... your "enter" selection is empty, because you already have data associated to that paths selection.
Besides that, it makes little sense using a proper "enter" selection for the texts, since the data is the same data bound to the groups.
Solution: the solution here, which is the idiomatic D3 for this situation, is creating an actual <g> selection.
We can do that by breaking the paths selection, and giving it another name:
pathGroups = groups.selectAll('.path_placeholder')
.data(groupIds, function(d) {
return +d;
})
.enter()
.append('g')
.attr('class', 'path_placeholder');
Then you can just do:
paths = pathGroups.append('path')
.attr('stroke', function(d) {
return color(d);
})
.attr('fill', function(d) {
return color(d);
})
.attr('opacity', 0)
texts = pathGroups.append('text')
.text(function(d) {
return d;
});
Here is the forked Plunker: https://next.plnkr.co/edit/31ZPXIvSI287RLgO

d3: acting on a class in response to an event

I'm binding data to a bunch of nodes using d3, and I would like to arrange it so that all of the nodes change dynamically when one of them is clicked on (or some other event). Based on my understanding of d3, I think it should work like this:
var nodes = svg.selectAll(".node")
.data(someData)
.enter()
.append("circle")
.attr("r", 5)
.attr("class", ".node")
.style("fill", "blue")
.on("click", function(d, i) {
svg.selectAll(".node").style("fill", function(e, j) {
if(someCondition(i, j))
return "red";
else
return "green";
});
});
But nothing happens when I click. Even the simpler code:
var nodes = svg.selectAll(".node")
.data(someData)
.enter()
.append("circle")
.attr("r", 5)
.attr("class", ".node")
.style("fill", "blue")
.on("click", function(d, i) {
svg.selectAll(".node").style("fill", "red");
});
(which I expect would turn all of the nodes red when one of them is clicked on) does not work.
There is an error in the way you are setting the class names for your circles by calling
.attr("class", ".node")
Doing it this way would set the attribute to class=".node" which is certainly not what you want. Moreover, this would not be a valid class name. See this answer for an explanation of what characters are allowed to form a class name. To select this class name you would have to do a svg.selectAll("..node") having two dots in your selector string.
Having said that, change you code to leave out the dot to make it work:
.attr("class", "node")
Lessons learned:
.attr() takes the attribute's value literally.
When applying a CSS selector, you prefix it with a dot to select a class name.
You need specify the "cx" and "cy" properties of a circle, otherwise you wont see anything.
var nodes = svg.selectAll(".node")
.data(someData)
.enter()
.append("circle")
.attr("r", 5)
//add cx and cy here:
.attr("cx", function(d) {return d+10;/*just an example*/})
.attr("cy", function(d) {return 2*d+10;/*just an example*/})
.attr("class", "node")
.style("fill", "blue")
.on("click", function(d, i) {
svg.selectAll(".node").style("fill", "red");
});

How to update selection with new data in D3?

I'm trying to edit the data of created circles in D3. Below my code is pasted of me creating a lot of circles based on some data from graphData.
Supposed I'd want to re-arrange my circles Y position with a new dataset, by transitioning them to their new destinations. How would perform this task? I've tried using attr.("cy", function(d){return yScale(parseFloat(d))} ) to update my Y-coordinates by adding data(graphData[i], function(d){return d;}) with my new data, but this does not work.
You can take a look at my JSFiddle: http://jsfiddle.net/RBr8h/1/
Instead of the for-loop in the following code I've created circles on 2 ticks of my X-axis. I have 3 sets of data and I've used to of them in the example in the fiddle. I'd like to able to use the 3rd dataset instead of the 2 first ones on both circles.
var circle;
for(var i = 0;i < graphData.length;i++){
circle = SVGbody
.selectAll("circle")
.data(graphData[i], function(d){return d;})
.enter()
.append("circle")
.attr("cx",xScale(0))
.attr("cy", yScale(minAxisY))
.attr("r",4)
.style('opacity', 0)
.transition()
.duration(1000)
.attr("cx", function(d){
return spreadCircles(i);
})
//.attr("cy", function (d, i){ return yScale(i); })
.style('opacity', 1)
.transition()
.duration(1500)
.attr("cy", function(d){return yScale(parseFloat(d))} );
Thank you for your help in advance!
To put some flesh on Lars comment, here is a FIDDLE leveraging the enter/update/exit pattern to help you out. I have altered and simplified your code (and data) just enough to demonstrate the principle.
function updateCircles(dataset,color) {
var circle = SVGbody
.selectAll("circle")
.data(dataset, function(d) { return d; });
circle
.exit()
.transition().duration(750)
.attr("r", 0)
.remove();
circle
.enter()
.append("circle");
circle
.attr("cx",function(d){return xScale(100);})
.attr("cy",function(d){return yScale(parseFloat(d))})
.attr("r",0)
.transition().duration(1500)
.attr("r",5)
.style("fill", color);
};
Update fiddle with data keyed off by index...so, circles just have their position updated.

Categories

Resources