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.
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 studying transitions in D3js, trying to get them working with a simple pie chart that is derived from a Mike Bostock example.
I want to transition the data in the pie from data to data2, but the chart does not update.
This is the transition statement:
path.selectAll("path").data(pie(data2)).transition().duration(2000);
What am I missing?
EDIT
I've got it working with the below. Now I'd like to understand why this is working. I understand the .attr("d",arc) part, but why do I need to selectAll("path")?
path.selectAll("path").data(pie(data2)).transition().duration(2000).attr("d",arc);
END EDIT
The complete code (JSFiddle here):
var width = 200,
height = 200,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var data = [2, 19, 18, 99, 100];
var data2 = [100, 1200, 20, 88, 12];
var pie, arc, svg, path, data;
var chartCanvas = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height);
var path = chartCanvas
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
function setupPieChart() {
pie = d3.layout.pie()
.value(function (d) {
return d;
})
.sort(null);
arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 20)
path
.selectAll("path")
.data(pie(data))
.enter()
.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.each(function (d) {
this._current = d;
}); // store the initial angles
console.log(path);
}
setupPieChart();
console.log(path);
path.selectAll("path").data(pie(data2)).transition().duration(2000);
.transition starts a transition for the attributes change(s) declared after it. You don't set / do anything after .transition, so there is nothing to interpolate over. In the original example from Mike, you'll see he sets the d attribute after starting the transition, specifying a custom interpolator (arcTween)
I immediately see you are missing some important part of the update process. You copied the original code, but you forgot the update part :-). I can tell, because I see in your code you store the initial angles.
Look again at the code here and try to understand the function arcTween. More information can be found here. You need an arc tween function to calculate the new angles based on the initial angles (which is why you stored the initial angles in the first place :-).
I won't do the fiddle at the moment, cause in my experience, you learn more if you try to understand the arc tween function (as I did here . This is a link to a personal project of mine, but feel free to copy code as you see fit).
You need to .selectAll("path") as those are the actual elements that will update. When doing d3, try to think of the chart elements as following: Elements that are not visible yet (which is enter collection), elements that are visible now (which can be seen as the update collection) and elements that can be removed (the exit collection).
You need to see those elements based on the data you want to visualize. D3 is data driven documents, so everything is in relation to the data you want to show on the screen: if you have data but no elements yet, you do an "enter" of elements. So you do a selection of elements that are not in the DOM yet, but will soon be, because you will bind them to the data you have.
If you already have elements on the screen, and the number of elements matches the data you need to show (for example: var data = [20 ,30 , 40], you got 3 pieces of data here for 3 div's on the screen, you got a matching amount), then d3 will update your selection (hence the update collection or update selection) so the elements properties match the data.
If you have more elements on the screen then there is data to show, then you can do an exit of elements (again, hence the exit selection or collection). I hope that made it a bit more clear and that it made sense as well :-)
I am updating a D3 stacked bar graph in combination with AngularJS. In this, all data is initially visible, and updates filter out undesired data. In the past it has worked without issue using this model:
data = [{
name: John Doe,
splits: [{distance: 1, time: 1234},]
},...]
Now I am attempting to to add one more bar to each stack using this model:
data = [{
name: John Doe
time: 12345,
splits: [{distance: 1, time: 1234},]
},...]
My issue is updating the data. My calculated values are recalculated correctly, such as the domain for scaling. A line for time update still only recognizes the data values from before the update (code snippet heavily truncated for brevity):
// Set ranges of values in axes
x.domain(data.map(function(d) { return d.name}));
y.domain([ min , max]);
// Y Axis is updated to the correct time range
chart.append('g').attr('class', 'y axis').call(yAxis).selectAll('text').style('font-family','Open Sans').style('font-size', '.9rem');
// data join / Select Athlete
var athlete = chart.selectAll(".athlete").data(data),
athdata = athlete.enter(),
console.log(data) // data is correct here
// add container for each athlete
athplace = athdata.append("g")
.attr("class", "athlete")
.attr("transform", function(d) { return "translate(" + x(d.name) + ",0)"; })
.text(function(d) { return d.name}),
// ENTER time
athplace.append('rect')
.attr('class', "time")
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.time.time); })
.attr("height", 0).transition().duration(300)
.attr("height", function(d) { return height-y(d.time); });
... enter splits & labels
// exit splits & athlete
splitlabels.exit().transition().duration(300).attr('opacity', 0).remove();
splits.exit().transition().duration(300).attr('height', 0).attr('opacity', 0).remove();
athlete.exit().transition().duration(300).attr('width',0).attr('opacity', 0).remove();
console.log(athlete) // data is still correct
// UPDATE time, this chain has the problem with data not being updated. "d" is equal to previous values
athlete.selectAll('rect.time')
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.time.time); })
.attr("height", function(d) { return height-y(d.time); });
Due to these errors, the updated columns represent the wrong data and produce the wrong visualization. I have been staring/testing at this all day trying to isolate the issue to what I have now. Could someone more experienced with D3 give me some insight?
Note: for those interested, this is all in an Angular directive where I $watch for changes to the data values, although I am 100% sure that that is not the issue.
Edit
Here is a JSFiddle that illustrates the same error as in my script. All of the code was extracted directly from the script exhibiting the issue. The interval update at the bottom imitates the swapping of data that would normally happen.
I played with your example for a little bit and made a few changes that may be of use.
First, I re-factored your global var's up top to inside your update function. By doing this, we are not "double-appending" where our x and y axis left off. This was seemingly drawing our chart on top of our old chart.
However, fixing this then gave me the new chart overlaying on top of our old. To fix this, I call d3.selectAll("svg > *").remove(); within update to remove all attached SVG elements and groupings, giving us a "clean slate" to render our new chart on. This solves the overlaying issue.
You mention this is shortened for brevity, so I hope this example can help with your actual application
Updated Fiddle
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.
I have a series of horizon graphs that have been created like this:
d3.select("body")
.selectAll(".horizon")
.data(metrics)
.enter()
.append("div")
.attr("class", "horizon")
.attr("id", function(d){return d.toString();})
.call(context.horizon().height(['75']));
where metrics is an array of metric objects.
I want to redefine the extent for one of those horizon objects.
I've tried calling the extent function on the one graph, but I'm not sure if I'm calling it, or even selecting it, correctly:
d3.select("#id-attribute")
.call(context.horizon().extent(function(d,i){return([0,1000]);}));
This sort-of seems to work, but the graph display gets screwed up, with additional whitespace being added below the graph and the motion of the graph not accounting for that and leaving the top of the graph unanimated. I suspect that it's in some way due to it being a new instance of the extent object, so I tried this:
d3.select("#id-attribute")
.call(extent(function(d,i){return([0,1000]);}));
but that generates: "ReferenceError: extent is not defined".
I've tried redefining the metric's extent function, effectively:
metrics[3].extent = function(d,i) {return([0,100]);};
but that causes the graph to be blank (although mousing over it reveals numbers in the readout), and causes its readout and the readouts of the graphs that appear below it to be blank when the mouse is not hovering over any of the graphs.
I honestly have no comprehension of how this stuff fits together, nor am I particularly experienced with JavaScript, so I'm not sure where my error lies. I'm totally out of my depth. Any tips would be greatly appreciated.
Do you need to redefine the extent after the initial call where the horizons are generated?
If not, try something like this:
d3.select("body")
.selectAll(".horizon")
.data(metrics)
.enter()
.append("div")
.attr("class", "horizon")
.attr("id", function(d){return d.toString();})
.call(context.horizon()
.height(['75'])
.extent(function(d,i){ //use this function to change extents
if (d.toString() == 'Whatever the name is'){
return [0,1000];
} else {
return [ <default min>, <default max>]
}
})
)
You can either set your own default extent, or use a function like d3.min()/d3.max() to get the value for the horizons. You just need to return an array with two elements.