d3.js data join shows strange result - d3.js

I am using d3v4 and within the html document I have 20 g.node elements, with 4 having the value: d.nameY === "responded No".
The following statement works correct:
d3.select("g.sankeyFrame.single")
.selectAll("g.node")
.filter( function(d) { return d.nameY === "responded No";})
.size();
It returns 4 as desired.
However, the following data join doesn't return the expected selection:
d3.select("g.sankeyFrame.single")
.selectAll("g.node")
.data(["responded No"], function(d){ return d.nameY;})
.size();
It returns 0, it should return all 4 g.node elements, where d.nameY === 4.
What possible reason prevents the data join from working?
(The code base is too large to publish here)

What possible reason prevents the data join from working?
No reason at all. It is working. And the result is not wrong, that's the expected behaviour. You have another problem here.
To show you the problem, let's create a very simple demo, which appends 20 <div>s, 4 of them with the datum "foo". Here is the code using your first approach, with filter:
var p = d3.select("body")
.selectAll(null)
.data(d3.range(20).map(function(d) {
return d < 4 ? "foo" : "bar"
}))
.enter()
.append("div")
.html(String);
var filtering = d3.selectAll("div")
.filter(function(d) {
return d === "foo"
}).size();
console.log("size is: " + filtering)
<script src="https://d3js.org/d3.v4.min.js"></script>
Now let's see your second approach:
d3.select("g.sankeyFrame.single")
.selectAll("g.node")
.data(["responded No"], function(d){ return d.nameY;})
.size();
You have two problems here:
The key function will not work. There is no property nameY in a simple, flat array like ["responded No"]. That's why your selection's size is 0.
There is just one element in that array. So, even if you remove the wrong key function, that selection will have just one element. Let's prove it:
var p = d3.select("body")
.selectAll(null)
.data(d3.range(20).map(function(d) {
return d < 4 ? "foo" : "bar"
}))
.enter()
.append("div")
.html(String);
var dataBinding = d3.selectAll("div")
.data(["baz"])
.size();
console.log("size is: " + dataBinding)
<script src="https://d3js.org/d3.v4.min.js"></script>
That point #2 is the most important here: the size of the update selection returned by data() is the size of its array.
Finally, to clearly show that the data() is working, let's see the datum of each <div>:
var p = d3.select("body")
.selectAll(null)
.data(d3.range(20).map(function(d) {
return d < 4 ? "foo" : "bar"
}))
.enter()
.append("div")
.html(String);
var dataBinding = d3.selectAll("div")
.data(["baz"])
.size();
d3.selectAll("div").each(function(d, i) {
console.log("div number " + i + " - datum: " + d)
})
<script src="https://d3js.org/d3.v4.min.js"></script>

Related

Get one element from d3js selection, by index

I've created a set of d3js elements based on an array of 3 elements:
var data = [[0,0,2],[0,23,5],[2,12,5]];
circleSet = svg.selectAll()
.data(data)
.enter().append('circle');
edit:
How can I select the second element by index?
The most natural way to manipulate just one element is using the filter function:
var data = [[0,0,2],[0,23,5],[2,12,5]];
var circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle');
var filteredCircleSet = circleSet
.filter(function (d, i) { return i === 1;})
// put all your operations on the second element, e.g.
.append('h1').text('foo');
Note that depending on what you do with the other elements you might use one of the two variants of this approach:
variant a): use the filter in the data function (to reduce the data and the appended elements)
variant b): use the filter to exclude instead of to include in order to remove the other elements at the end
See also Filter data in d3 to draw either circle or square
One other way to do it is to use the selection.each method: https://github.com/mbostock/d3/wiki/Selections#wiki-each
By using an if statement with the corresponding index you can create a block for one element.
E.g.
var data = [[0,0,2],[0,23,5],[2,12,5]];
var circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle')
.each(function (d, i) {
if (i === 1) {
// put all your operations on the second element, e.g.
d3.select(this).append('h1').text(i);
}
});
In d3 v4 and above, you can use Selection.nodes(). Assuming i is the index number you want:
d3.select(someSelection.nodes()[i])
It's a natural one-liner, and it's arguably more readable: you're obviously just getting the node at i in the order, as a D3 selection.
It looks like it'd be more efficient than the alternatives, which involve looping through the entire selection with .each(). So, you might think this is O(1), while the other options are O(n).
Unfortunately, Selection.nodes() itself includes an each loop, so it's also O(n) (not that it's likely to matter in real life unless you call this thousands of times on selections with thousands of nodes):
var nodes = new Array(this.size()), i = -1;
this.each(function() { nodes[++i] = this; });
return nodes;
However, this way you can separate the looping from the getting, which could be useful if efficiency is a major concern.
For example, if you want to loop through each() in selection A and get the item in the same position from selection B, and you want to avoid loops-within-loops because those selections can be huge and you call this many times, you could structure it like this, which would be O(2n) instead of O(n^2):
var selectionBArray = selectionB.nodes()
selectionA.each(function(d, i) {
var iFromSelectionA = this
var iFromSelectionB = d3.select(selectionBArray[i])
})
...or if you're using arrow functions to preserve this context:
var selectionBArray = selectionB.nodes()
selectionA.each((d, i, nodes) => {
var iFromSelectionA = d3.select(nodes[i])
var iFromSelectionB = d3.select(selectionBArray[i])
})
You could even (ab)use Selection._groups, but I wouldn't recommend using a private property like that since it'll break if a D3 update renamed the _groups property, like this update did.
Use the preset function i variable, which references the index of the array object.
var data = [[0,0,2],[0,23,5],[2,12,5]];
circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle')
.attr('fill',function(d,i){i === 1 ? return 'red' : return 'black' };
Find more on array structure references in d3.js at this tutorial
You can also encode each element you append by utilizing the count of the i index when assigning a class.
var data = [[0,0,2],[0,23,5],[2,12,5]];
circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle')
.attr("class",function(d,i){ return "yourclass item" + i })
var theSecondElement = d3.select(".item1")
Last, you could use the .each method and a conditional to target a specific element
circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle')
.each(function (d, i) {
if (i === 1) {
var that = this;
(function textAdd() {
d3.select(that).append('h1').text(i);
)();
}
});

Reload nested data in D3.js

I do not manage to update a bar-chart with nested data in D3.js with new data.
I have nested data of the form:
data = [[1,2,3,4,5,6],[6,5,4,3,2,1]];
I managed to visualize the data by first appending a group for every subarray.
In the groups I then add the arrays as data (simplified):
function createGraph(l, svg){
var g = svg.selectAll("g")
.data(l)
.enter().append("g");
var rect = g.selectAll("rect)
.data(function(d){return d;})
.enter().append("rect")
. ...
}
However, when call the function again with different data, nothing happens.
It seems like in the second row, the rects do not get updated.
I have created a full example over at jsBin: http://jsbin.com/UfeCaGe/1/edit?js,output
A little more explanation of Lars' bug-catch, since I'd already started playing around...
The key was in this section of the code:
var group = svg.selectAll("g")
.data(l)
.enter().append("g");
The variable group is assigned the enter selection, not the raw selection. Then in the next line:
var bar = group.selectAll("rect")
.data(function(d){
return d;
});
You end up defining bar as only the rectangles that are children of just-entered groups. So even though you were handling update correctly for the rectangles, that whole section of code wasn't even running. You need to save the group selection before branching the chain to deal with entering groups:
var group = chart.selectAll("g")
.data(dt);
group.enter().append("g");
var bar = group.selectAll("rect")
.data(function(d){
return d;
});
Also, you're missing a j in your function declaration in your update. And you can reduce code duplication by putting your rectangle update code after your rectangle enter code, and then any attributes that get set in the update don't have to be specified for enter. (Some older examples don't use this pattern, because the original versions of d3 didn't automatically transfer newly-entered elements to the main selection.)
// enter
bar.enter().append("rect")
.attr("fill", function(d,i,j){
return colors(j);})
.attr("height", 0);
// update
bar.attr("transform", function(d, i, j) {
x = "translate("+(i*2.2*w+j*w)+",0)";
return x; })
.transition()
.duration(750)
.attr("width", w)
.attr("height", function(d){return d*10;});

D3.js graph displaying only one dataset

I having trouble getting the data on the graph. I only get one data set bar in.
You can see it here : http://infinite-fjord-1599.herokuapp.com/page2.html
But when I console.log the foreach for it. It displays all the objects:
data.days.forEach(function(d) {
d.ages = ageNames.map(function(name) { return {name: name, value: +d.values[name]}; });
console.log(d.ages);
});
The code on jsFiddle. http://jsfiddle.net/arnir/DPM7y/
I'm very new to d3.js and working with json data so I'm kinda lost here. I took the example of the d3.js example site and modified it.
See the updated fiddle here: http://jsfiddle.net/nrabinowitz/NbuFJ/4/
You had a couple of issues here:
Your x0 scale was set to a domain that displayed a formatted date, but when you were calling it later you were passing in d.State (which didn't exist, so I assume it was a copy/paste error). So the later days were being rendered on top of the first day.
There was a mismatch between the way you were selecting the group g element and the way you were appending it - not actually a root cause here, but likely to cause problems later on.
To fix, move the date formatting to a different function:
function formatDate(d) {
var str = d.modified;
d.date = parseDate( str.substring(0, str.length - 3) );
var curr_month = d.date.getMonth() + 1;
var curr_date = d.date.getDate();
var nicedate = curr_date + "/" + curr_month;
return nicedate;
}
and then use the same function for the scale setup:
x0.domain(data.days.map(formatDate));
and the transform (note the fix in the selector and class here as well):
var state = svg.selectAll("g.day")
.data(data.days)
.enter().append("g")
.attr("class", "day")
.attr("transform", function(d) {
return "translate(" + x0(formatDate(d)) + ",0)";
});
There are a couple of small things that threw you off. First, the domain of the x0 scale should be an array of datetime objects, not an array of strings:
x0.domain(data.days.map(function(d) {
var str = d.modified;
d.date = parseDate( str.substring(0, str.length - 3) );
return d.date;
}));
will return datetimes, not strings like it was before (minor nitpick: really not a fan of this use of map, I would add the date property separately in a forEach function as the data is loaded).
Second, x0 needs to be passed a property that actually exists:
var state = svg.selectAll(".state")
.data(data.days)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + x0(d.date) + ",0)"; });
Before, you were using x0(d.state) which is a vestige from the grouped bar example (several others still exist; I've changed the minimum to get your project working). Since the value didn't exist, all of the rectangles were getting drawn over each other.
Additionally, we need to format the axis labels so we aren't printing out the entire datetime object all over the labels:
var xAxis = d3.svg.axis()
.scale(x0)
.orient("bottom")
.tickFormat(d3.time.format("%m-%d"));
Finally, I noticed that the newest dates were being printed on the left instead of the right. You could sort the results of data.days.map( ... ) to fix that, I just reversed the range of x0:
var x0 = d3.scale.ordinal()
.rangeRoundBands([width, 0], .1);
fixed files

Confused about data joins, select and selectAll

I'm confused about data joins.
I have an entering group element, called gEnter, to which I append
gEnter.append("g").attr("class", "dataLabels");
dataLabels is the container element for each data label I will make.
g is the update selection for the original group element. I bind my data like this:
var dataLabels = g.select(".dataLabels")
.selectAll(".dataLabel")
.data(function(d) {return d;});
where d is coming from the parent g element. For each new data point I append a .dataLabel, and give it a starting position 30 pixels up from the axis:
var dataLabelsEnter = dataLabels.enter()
.append("g")
.attr("class", "dataLabel")
.attr("transform", function(d, i) { return "translate("+ (xScale(d.category) + (xScale.rangeBand() / 2)) +","+(yScale(0) - 30)+")"; });
Each .dataLabel is itself a container for two text elements, so I append them for each new data point:
dataLabelsEnter.append("text")
.attr("class", "category")
.attr("text-anchor", "middle")
.style("font-weight", function(d, i) {
return (d.category == 'Total')
? 'bold'
: 'normal';
})
.text(function(d) {return d.category;});
dataLabelsEnter.append("text")
.attr("class", "value")
.attr("text-anchor", "middle")
.attr("transform", "translate(0,20)")
.style("font-weight", "bold")
.style("fill", function(d, i) {
return (d.count >= 0)
? '#1f77b4'
: '#BB1A03';
})
.text(function(d) {
var accounting = d3.format(",");
return (d.count >= 0)
? '+$' + accounting(d.count)
: '-$' + accounting(-d.count);
});
I then move to my update code, where things get interesting. First, I update the position of the container .dataLabel element. This works well:
dataLabels
.transition()
.duration(duration)
.attr("transform", function(d, i) {return "translate("+ (xScale(d.category) + (xScale.rangeBand() / 2)) +","+( yScale(d3.max([d.count,0])) - 30)+")"; });
Now I want to update the values of my labels. I try this:
dataLabels
.selectAll(".value")
.text(function(d, i) {
var accounting = d3.format(",");
// return d.count;
return (d.count >= 0)
? '+$' + accounting(d.count)
: '-$' + accounting(-d.count);
});
but it doesn't work. I try rebinding the data, using a .data(function(d){return d;}), but to no avail. No matter what I do, even if the data updates, here it's still the same as the initial draw. However, if I switch to
dataLabels
.select(".value")
.text(function(d, i) {
var accounting = d3.format(",");
// return d.count;
return (d.count >= 0)
? '+$' + accounting(d.count)
: '-$' + accounting(-d.count);
});
it works.
Can anyone explain why the latter selection gets the updated the data, but the former selection doesn't? I've read Mike Bostock's recent article on selections, but am still a little confused. I believe it has something to do with this sentence from the article:
Only selectAll has special behavior regarding grouping; select preserves the existing grouping.
Perhaps selectAll is creating new groups from each .dataLabel element, but the data is not being bound to them? I'm just not sure.
The difference is that selection.select propagates data from parent to child, whereas selection.selectAll does not. Read the paragraph you quoted again, in Non-Grouping Operations section:
Only selectAll has special behavior regarding grouping; select preserves the existing grouping. The select method differs because there is exactly one element in the new selection for each element in the old selection. Thus, select also propagates data from parent to child, whereas selectAll does not (hence the need for a data-join)!
So, when you did the data join on dataLabels, you’ve updated the data on the parent elements. But when you call dataLabels.selectAll(".value"), it doesn’t propagate data, so you were getting the old child data. If you switch to dataLabels.select(".value"), it propagates data to the selected children, so you get the new data again.
You could have propagated the data using selection.data, too, but since each label has one value element here, using selection.select is easier.
(Also, you might want to specify a key function.)

d3 append and enter issues

So I am having a problem when following a very simple d3 tutorial. Essentially I am attempting to add SVG circle elements by binding data to the current svg circle elements then calling enter.append As shown below:
var svg =d3.select("body").selectAll("svg");
var circle = svg.selectAll("circle");
var w=window.innerWidth;
console.log(circle);
circle.data([500,57,112,200,600,1000]);
circle.enter().append("circle")
.attr("cy",function(d) {
return (d)
})
.attr("cx", function(i){
return (i*100)
})
.attr("r", function(d) {
return Math.sqrt(d);
})
Which seems like it would add 3 new circle elements (because I already have 3 created). However, instead of getting these 3 new circle elements added, I am running into this error:
Uncaught TypeError: Object [object SVGCircleElement],[objectSVGCircleElement],[object SVGCircleElement] has no method 'enter'
I have done essentially the same thing with paragraphs, and it seems to work fine:
var p =d3.select("body").selectAll("p")
.data([4, 8, 15, 16, 23, 42])
.text(function(d) { return "I'm number " + d + "!"; });
//Enter
p.enter().append("p")
.text(function(d) { return "I'm number " + d + "!"; })
.style("color", function(d, i) {
return i % 2 ? "#000" : "#eee";
});
However as soon as I try to add SVG Elements into it, I continue to get the same error.
It seems like there should be just a syntax error or something, but I have gone through the code 5 trillion times, and can't find anything.
Any help is much appreciated, and thank you before hand for your time.
Isaac
You want to call enter on the result of circle.data instead of circle itself.
var circle = svg.selectAll("circle");
circle.data([500,57,112,200,600,1000]).enter().append("circle");
You did this correctly in your p example by storing the return of data in the p variable. Whereas in your circle example, you are storing the return of d3.select.selectAll in your circle variable.

Resources