I'm working on my first mid-scale d3 project right now after having run through the tutorials. I understand scales, enter, update, and exit, so I'm pretty confused about a problem I'm running into.
I have an array of JSON objects with two fields, year and number. I am creating a bar chart with this array with the following code:
var bar = chart.selectAll('g')
.data(yearData)
.enter().append('g')
.attr('transform', function(d, i) {
console.log(i);
return 'translate(' + i * barWidth + ',0)'; });
My confusion stems from the fact that the console.log statement in this code block outputs 27 as its first value. In other words, d3 is skipping elements 0 - 26 of my array. Why could this be??
Thanks for your help.
This is most likely because you already have g elements on your page (e.g. from adding an axis). These are selected and matched with data, so the enter selection doesn't contain everything you expect.
One solution is to assign a class to these elements and select accordingly:
var bar = chart.selectAll('g.bar')
.data(yearData)
.enter().append('g')
.attr("class", "bar")
// ...
Much more detail on this in the second half of this tutorial.
Related
I am experimenting with a stacked bar chart in d3js and ran into enter exit selection difficulties. I used the d3.stack to get two arrays organized by keys, then I tried to follow the general update pattern. The problem I'm having now is the data is not getting updated when I click a different state in the dropdown menu. Here's the problem code and a link to the full project: http://plnkr.co/edit/8N8b2yUYRF9zqRkjkIiO?p=preview
var series = g.append("g")
var seriesready =
series.selectAll("g")
.data(stack(data))
.enter().append("g")
.attr("fill",function(d){console.log(d); return z(d.key)}) //not logging
when I update the bar chart
var rectangles =
seriesready.selectAll("rect")
.data(function(d){return d})
rectangles.exit().remove()
rectangles.enter().append("rect")
.attr("width", x.bandwidth())
.transition()
.duration(1500)
.attr("transform", function(d) {console.log(d); return "translate(" + x(d.data.Date) + ",0)"; })
.attr("height", function(d) {
return height - y(d[1]-d[0]);
})
.attr("y", function(d) {
return y(d[1]-d[0]);
});
I also think I'm getting confused as to what selections should be removed or added. Would really appreciate any pointers. Data viz is fun to work with, but I still haven't fully grasped data binding yet.
I have not made the switch to version 4 yet, but the data binding methodology is the same i think.
You need to define a key function as the second parameter to the .data() function.
A key function may be specified to control which datum is assigned to
which element, replacing the default join-by-index.
https://github.com/d3/d3-selection/blob/master/README.md#selection_data
Your updated code
http://plnkr.co/edit/wwdjJEflZtyACr6w9LiS?p=preview
The changed code:
var seriesUpdate = series.selectAll("g")
.data(stack(data),d=>d)
var seriesready = seriesUpdate.enter().append("g")
.attr("fill",function(d){return z(d.key)})
seriesUpdate.exit().remove()
When binding data to elements, D3 calculates what data is new/existing/removed in relation to the selection. By default it does this by data index - the size of the input array. Since the computed stack data for michigan and ohio both return 2 sets of data (injured and killed), D3 views this as "same" data, thus it's an update.
If you define a key function, D3 recognizes the computed stack data for michigan and ohio as being "different" data, thus it's an enter.
With a key function, when you select Ohio first, the enter selection is size 2 with Ohio. If you then select Michigan, the enter selection is size 2 with Michigan, and the exit selection is size 2 with Ohio.
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.
Hello nice people of SO,
I'm pretty new to javascript, JSON and D3.js and I'm trying to build a multi-line time series chart. My data comes as JSON from my server like this:
{
"t": ["2014-08-16T16:15:00", "2014-08-16T16:20:00", "2014-08-16T16:25:00", ...],
"todd": [0,0,1,2,3,2,1,0,0, ...],
"pete": [3,2,1,0,4,4,0,0,0, ...]
}
This I store in an array called 'dataset'. The datetimes are subsequently parsed. 't', 'todd' and 'pete' arrays are of equal length.
Ultimately I want the time (t) values to be used for the x-axis, the other two as y values over time. Parsing the data works, however I can't get it to display properly. What works so far is:
A: display my values one after the other without a reference to time, so I get two nice, colored lines but the x-axis holds no information, as it just displays 0 to N.
OR
B: have the x-axis show as a timeline based on the "t"-values (this tells me that the parsing works as it should). However, I can't get my two lines to show.
In Variant A I was simply returning the index as x value like so:
var line = d3.svg.
.x(function(d,i) {
return i;
})
...
Now I'm trying to return the datetime from my "t"-array:
var line = d3.svg.
.x(function(d,i) {
return dataset.t[i];
})
...
However, nothing is displayed. I get no errors at all in the console. I tried console logging the value of 'dataset.t[i]' but nothing shows. It's as if the code isn't even running so here is the for loop that calls the above code:
for (var key in dataset) {
if (dataset.hasOwnProperty(key) && key!="t") {
data=dataset[key];
svg.append("svg:path").attr("d", line(data))
.style("stroke", color(key))
.style("stroke-width", 2);
//BELOW IS IRRELEVANT TO MY QUESTION, JUST FOR COMPLETENESS...
svg.append("rect")
.attr("x", w - 65)
.attr("y", n*25)
.attr("width", 10)
.attr("height", 10)
.style("fill", color(key));
svg.append("text")
.attr("x", w - 50)
.attr("y", n * 25 + 10)
.attr("height",30)
.attr("width",100)
.style("fill", color(key))
.text(key);
n+=1; // counter up
}
};
I can tell this is running as the 'rect' and 'text' elements are added to the chart.
The full code is here: http://pastebin.com/tbS7rD7p
By now I can't see straight anymore so I'm really looking forward to your ideas. Many thanks in advance!
Cheers, Ben
Thanks, in code veritas, for your input. I'm in the process of optimizing the code in the way you suggested. However, it seems you do not need the .data() method with lines as you allocate the data via .x() and .y() methods for these (if I understand correctly).
Anyway, in the end the solution was to change:
.x(function(d,i) {return dataset.t[i];})
to
.x(function(d,i) {return x(dataset.t[i]);})
which is mentioned in pretty much every tutorial but which I, wisely ^^, chose to ignore because it seemed pointless to me. If anybody can comment on that and enlighten me, I'd be grateful.
Cheers!
Hello guys I am trying to draw a grouped bar chart with d3js
I want through several tutorials and finally tried with this code:
<script>
var data = [[10,20,30,40,50],[20,20,40,50,60]];
var canvas = d3.select("body")
.append("svg")
.attr("width",500)
.attr("height",500);
canvas.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("height",function(d){return d*10;})
.attr("width",50)
.attr("x",function(d,i){return i*60})
.attr("y",function(d,i){return 500-(d*10);})
.attr("fill","red");
</script>
There is some error. How to set data for grouped chart..
To answer your first question, the error you're using is coming from the data structure that you've chosen. The error is happening due to the way that the .data(data).enter() clause works.
The whole clause will implicitly iterate over the elements of the first level in your outter array. That is each element d that is passed to
.attr("height",function(d){...) .attr("x",function(d)...) and .attr("y",function(d)...) is an array. So, the internals of each function are operating with constants on the array, which makes no sense. To clarify in javascript what does d * [1,2,3,4] mean?
To answer your second question. If you want to create a grouped bar chart check out this link: http://bl.ocks.org/mbostock/3887051.
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.