First of all sorry if my English is difficult to understand, I'll try my best...
I am rather new to D3.js and I'm trying to create a D3 grouped bar chart using nested data. I have looked at some solutions shared here, but they only show one-level grouping. In my case, data will e from a csv file that has this data structure:
groups,categories,value 1,value 2,value 3
group 1,A,61.0158803,25.903359,13.08076071
group 1,B,71.27703826,21.0180133,7.70494844
group 1,C,82.70203982,13.52731445,3.770645737
group 2,A,58.85721523,28.25939061,12.88339417
group 2,B,71.39695487,20.66010982,7.942935308
group 2,C,82.22389321,13.68924542,4.08686137
The chart is intended to have two x axis, one for the groups (level 0) and one for the categories (level 1).Values 1 to 3 will display as grouped bars for each catergory, and the categories will be displayed within the corresponding group.
The structure of the chart should be:
value 1 | value 2 | value 3 | value 1 | value 2 | value 3 | value 1 | value 2 | value 3 |
| category A | category B | category C |
| group 1 |
and the same for group 2, placed contiguous.
The problem is with the code I am working on, I get the right axis but data corresponding two both groups are shown, one on top of the other, in each group area. I am not able to link the data on the categories to their corresponding group in orther to draw them where it corresponds.
Here is the code I've got so far:
var x0 = d3.scale.ordinal()
.rangeRoundBands([0,width], 0);
var x1 = d3.scale.ordinal()
.rangeRoundBands([0,width]);
var x2 = d3.scale.ordinal();
var y = d3.scale.linear()
.range([height,0]);
var color = d3.scale.category10();
var x0Axis = d3.svg.axis()
.scale(x0)
.orient("bottom");
var x1Axis = d3.svg.axis()
.scale(x1)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select(".chart")
.append("svg")
.attr("class", "svg")
.attr("viewBox", "" + margin* -1 + " " + margin* -1 + " " + (width + margin*2) + " " + (height + margin *2) + "")
.attr ("preserveAspectRatio", "xMidYMid")
.attr("width", "100%")
.attr("height", "100%")
d3.csv("../data/EQ01.csv", function(error, data){
if (error) throw error;
var seriesNames = d3.keys(data[0]).filter(function(key) { return key !== "categories" && key !== "groups";});
data.forEach(function(d) {
d.values = seriesNames.map(function(name) { return {
xValue: name,
yValue: +d[name]
};
});
});
nested = d3.nest()
.key(function(d) { return d.groups})
.key(function(d) { return d.categories})
.entries(data);
y.domain([0, d3.max(data, function(d) { return d3.max(d.values, function(d) { return d.yValue; }); })]);
x0.domain(nested.map(function(d) {return d.key;}));
x1.domain(data.map(function(d) { return d.categories; })).rangeRoundBands([0, x0.rangeBand() ], 0.1);
x2.domain(seriesNames).rangeRoundBands([0, x1.rangeBand()], 0);
svg.append("g")
.attr("class", "x0 axis")
.attr("transform", "translate(0," + (height+30) + ")")
.call(x0Axis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
var group = svg.selectAll(".group")
.data(nested)
.enter().append("g")
.attr("class", "group")
.attr("transform", function(d) { return "translate(" + x0(d.key) + ",0)"; });
group.append("g")
.attr("class", "x1 axis")
.attr("transform", "translate(0," + height + ")")
.call(x1Axis);
var category = group.selectAll(".category")
.data(data)
.enter().append("g")
.attr("class", "category")
.attr("transform", function(d) { return "translate(" + x1(d.categories) + ",0)"; });
category.selectAll("rect")
.data(function(d) { return d.values; })
.enter().append("rect")
.attr("width", x2.rangeBand())
.attr("x", function(d) { return x2(d.xValue); })
.attr("y", function(d) { return y(d.yValue); })
.attr("height", function(d) { return height - y(d.yValue); })
.style("fill", function(d){return color(d.xValue)})
Many thanks in advance for the help!
First of all sorry if my English is difficult to understand, I'll try my best...
I am rather new to D3.js and I'm trying to create a D3 grouped bar chart using nested data. I have looked at some solutions shared here, but they only show one-level grouping. In my case, data will e from a csv file that has this data structure:
groups,categories,value 1,value 2,value 3
group 1,A,61.0158803,25.903359,13.08076071
group 1,B,71.27703826,21.0180133,7.70494844
group 1,C,82.70203982,13.52731445,3.770645737
group 2,A,58.85721523,28.25939061,12.88339417
group 2,B,71.39695487,20.66010982,7.942935308
group 2,C,82.22389321,13.68924542,4.08686137
The chart is intended to have two x axis, one for the groups (level 0) and one for the categories (level 1).Values 1 to 3 will display as grouped bars for each catergory, and the categories will be displayed within the corresponding group.
The structure of the chart should be:
value 1 | value 2 | value 3 | value 1 | value 2 | value 3 | value 1 | value 2 | value 3 |
| category A | category B | category C |
| group 1 |
and the same for group 2, placed contiguous.
The problem is with the code I am working on, I get the right axis but data corresponding two both groups are shown, one on top of the other, in each group area. I am not able to link the data on the categories to their corresponding group in orther to draw them where it corresponds.
Here is the code I've got so far:
var x0 = d3.scale.ordinal()
.rangeRoundBands([0,width], 0);
var x1 = d3.scale.ordinal()
.rangeRoundBands([0,width]);
var x2 = d3.scale.ordinal();
var y = d3.scale.linear()
.range([height,0]);
var color = d3.scale.category10();
var x0Axis = d3.svg.axis()
.scale(x0)
.orient("bottom");
var x1Axis = d3.svg.axis()
.scale(x1)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select(".chart")
.append("svg")
.attr("class", "svg")
.attr("viewBox", "" + margin* -1 + " " + margin* -1 + " " + (width + margin*2) + " " + (height + margin *2) + "")
.attr ("preserveAspectRatio", "xMidYMid")
.attr("width", "100%")
.attr("height", "100%")
d3.csv("../data/EQ01.csv", function(error, data){
if (error) throw error;
var seriesNames = d3.keys(data[0]).filter(function(key) { return key !== "categories" && key !== "groups";});
data.forEach(function(d) {
d.values = seriesNames.map(function(name) { return {
xValue: name,
yValue: +d[name]
};
});
});
nested = d3.nest()
.key(function(d) { return d.groups})
.key(function(d) { return d.categories})
.entries(data);
y.domain([0, d3.max(data, function(d) { return d3.max(d.values, function(d) { return d.yValue; }); })]);
x0.domain(nested.map(function(d) {return d.key;}));
x1.domain(data.map(function(d) { return d.categories; })).rangeRoundBands([0, x0.rangeBand() ], 0.1);
x2.domain(seriesNames).rangeRoundBands([0, x1.rangeBand()], 0);
svg.append("g")
.attr("class", "x0 axis")
.attr("transform", "translate(0," + (height+30) + ")")
.call(x0Axis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
var group = svg.selectAll(".group")
.data(nested)
.enter().append("g")
.attr("class", "group")
.attr("transform", function(d) { return "translate(" + x0(d.key) + ",0)"; });
group.append("g")
.attr("class", "x1 axis")
.attr("transform", "translate(0," + height + ")")
.call(x1Axis);
var category = group.selectAll(".category")
.data(data)
.enter().append("g")
.attr("class", "category")
.attr("transform", function(d) { return "translate(" + x1(d.categories) + ",0)"; });
category.selectAll("rect")
.data(function(d) { return d.values; })
.enter().append("rect")
.attr("width", x2.rangeBand())
.attr("x", function(d) { return x2(d.xValue); })
.attr("y", function(d) { return y(d.yValue); })
.attr("height", function(d) { return height - y(d.yValue); })
.style("fill", function(d){return color(d.xValue)})
Many thanks in advance for the help!
Share Improve this question asked Jun 7, 2016 at 21:51 VidimarVidimar 331 silver badge4 bronze badges 2-
I think it should be
.data(function(d) { return d.values; })
for the'.category'
groups. If this doesn't work, can you set-up a jsfiddle or similar? – tarulen Commented Jun 8, 2016 at 11:59 - Hi @tarulen, and thanks for the answer! I have tried that and it doesn't seem to work. Here is a JSFiddle link – Vidimar Commented Jun 8, 2016 at 14:37
1 Answer
Reset to default 8The issue is that you are not joining correctly your data with your elements.
We need to build different scales in order to obtain the correct rangeBand
value.
var x_groups = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var x_categories = d3.scale.ordinal();
var x_values = d3.scale.ordinal();
I created a nested data structure that will contain everything we need for our grouped bar chart approach.
var nested = d3.nest()
.key(function(d) {
return d.groups;
})
.key(function(d) {
return d.categories;
})
.rollup(function(leaves) {
return [{
key: 'v-a',
value: leaves[0]['value 1']
}, {
key: 'v-b',
value: leaves[0]['value 2']
}, {
key: 'v-c',
value: leaves[0]['value 3']
}];
})
.entries(data);
Next lets configure our scales with the information we just got.
x_groups.domain(nested.map(function(d) {
return d.key;
}));
//var categories = ['A', 'B', 'C'];
var categories = nested[0].values.map(function(d, i) {
return d.key;
});
x_categories.domain(categories).rangeRoundBands([0, x_groups.rangeBand()]);
//var values = ['value 1', 'value 2', 'value 3'];
var values = nested[0].values[0].values.map(function(d, i) {
return d.key;
});
x_values.domain(values).rangeRoundBands([0, x_categories.rangeBand()]);
Then we can finally start our data join. You can see that when we enter a new level of info we need to set the data
function correctly.
var groups_g = svg.selectAll(".group")
.data(nested)
.enter().append("g")
.attr("class", function(d) {
return 'group group-' + d.key;
})
.attr("transform", function(d) {
return "translate(" + x_groups(d.key) + ",0)";
});
var categories_g = groups_g.selectAll(".category")
.data(function(d) {
return d.values;
})
.enter().append("g")
.attr("class", function(d) {
return 'category category-' + d.key;
})
.attr("transform", function(d) {
return "translate(" + x_categories(d.key) + ",0)";
});
var categories_labels = categories_g.selectAll('.category-label')
.data(function(d) {
return [d.key];
})
.enter().append("text")
.attr("class", function(d) {
return 'category-label category-label-' + d;
})
.attr("x", function(d) {
return x_categories.rangeBand() / 2;
})
.attr('y', function(d) {
return height + 25;
})
.attr('text-anchor', 'middle')
.text(function(d) {
return d;
})
var values_g = categories_g.selectAll(".value")
.data(function(d) {
return d.values;
})
.enter().append("g")
.attr("class", function(d) {
return 'value value-' + d.key;
})
.attr("transform", function(d) {
return "translate(" + x_values(d.key) + ",0)";
});
var values_labels = values_g.selectAll('.value-label')
.data(function(d) {
return [d.key];
})
.enter().append("text")
.attr("class", function(d) {
return 'value-label value-label-' + d;
})
.attr("x", function(d) {
return x_values.rangeBand() / 2;
})
.attr('y', function(d) {
return height + 10;
})
.attr('text-anchor', 'middle')
.text(function(d) {
return d;
})
var rects = values_g.selectAll('.rect')
.data(function(d) {
return [d];
})
.enter().append("rect")
.attr("class", "rect")
.attr("width", x_values.rangeBand())
.attr("x", function(d) {
return 0;
})
.attr("y", function(d) {
return y(d.value);
})
.attr("height", function(d) {
return height - y(d.value);
})
.style("fill", function(d) {
return color(d.key);
});
Working plnkr: https://plnkr.co/edit/qGZ1YuyFZnVtp04bqZki?p=preview