0

I'm trying to build a stacked bar chart in D3js. I have problems to set properly y and y0 attributes and draw the bars on their right positions. Probably I have a calculation mistake but I cannot find it. This is the link to the example code FIDDLE The scenario is:

  1. I group the data first by "period" and the periods are shown on xAxis
  2. Then I have grouping by "type" - MONTH and ENTRY which should be stacked bars in different colors.
  3. The sum "amount" for each type per each period is shown on yAxis.

I use nest function with 2 keys to structure the data. The problem appears when I draw the bars in the actual stacked bar chart. I'm not sure whether the problem is in the way I access the data (key and values) or in the way I set the attributes "y" and "height".

selection.selectAll("rect")
    .data(function (d) { return d.values; })
    .enter().append("rect")
    .attr("width", x.rangeBand())
    .attr("y", function (d) { return y(d.values); })
    .attr("height", function (d) { return y(d.y0) + y(d.values); })
    //.attr("height", function (d) { return y(d.y0) - y(d.values); })
    .style("fill", function (d) { return color(d.key); })

The obvious errors are that one of the bars is hidden behind another one. And the second bar is under the xAxis.

I'm beginner in d3js and I cannot find the solution. Can somebody help me?

Matteo
  • 37,680
  • 11
  • 100
  • 115
Ils
  • 13
  • 4

1 Answers1

0

I can see a few things:

  1. It looks like you're overcomplicating the nest. You should only need to nest a single level.
  2. The max value that you're calculating will only ever be the maximum of a single element of the stack, when you actually want the maximum to be the total of the stack.
  3. The group elements that you're creating (g), seem to be grouped the "wrong" way. You generally want to group the same "bit" of each stack. That is, you want the first rect of each stack to be in the same group as the other first rects. Then the second one in each stack will be grouped with the other second rects and so on. This is probably due to the nesting error in the first point.
  4. You actually need to calculate the valueOffset, which you've got in your fiddle, but is commented out. This value is used to set the relative position when constructing the stack.

To help, I've put together what seems right based on what you've written. Check out the snippet below.

    var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 400 - margin.left - margin.right,
    height = 400 - margin.top - margin.bottom;
            
var color = d3.scale.category10();

var data = [
    {
      "period":201409,
      "type":"MONTH",
      "amount":85.0
    },
    {
      "period":201409,
      "type":"ENTRY",
      "amount":111.0
    },
    {
      "period":201410,
      "type":"MONTH",
      "amount":85.0
    },
    {
      "period":201410,
      "type":"ENTRY",
      "amount":55.0
    }   
];
    
var x = d3.scale.ordinal().rangeRoundBands([0, width], .1, 0);
var y = d3.scale.linear().range([height, 0]);


var xAxis = d3.svg.axis()
                .scale(x)
                .orient("bottom");

var yAxis = d3.svg.axis()
                .scale(y)
                .orient("left").ticks(10);

var svg = d3.select("#chart")
                .append("svg")
                .attr("width", width + margin.left + margin.right)
                .attr("height", height + margin.top + margin.bottom)
                .append("g")
                .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
                
            data.forEach(function(d) {
                d["period"] = d["period"];
                d["amount"] = +d["amount"];
                d["type"] = d["type"];
            });

var nest = d3.nest()
                .key(function(d) { return d["type"];});

var dataByType = nest.entries(data);
//var max = d3.max(dataByGroup, function(d) { return d3.sum(d.values, function(e) { return e.values; }); })

            //console.log("dataByGroup", dataByGroup);  
var stack = d3.layout.stack()
                .values(function(d) { return d.values; })
                .x(function(d) { return d.period; })
                .y(function(d) { return d.amount; })
                .out(function(d, y0) { 
                  d.valueOffset = y0; 
                });
            
//data: key: group element, values: values in each group
stack(dataByType);
var yMax = d3.max(dataByType, function(type) { return d3.max(type.values, function(d) { return d.amount + d.valueOffset; }); });

color.domain(dataByType[0].values.map(function(d) { return d.type; }));
x.domain(dataByType[0].values.map(function(d) { return d.period; }));
y.domain([0, yMax]);
            
svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

svg.append("g")
    .attr("class", "y axis")
    .call(yAxis)
    .append("text")
    .attr("transform", "rotate(-90)")
    .attr("y", 3)
    .attr("dy", ".71em")
    .style("text-anchor", "end");

var selection = svg.selectAll(".group")
    .data(dataByType)
  .enter().append("g")
    .attr("class", "group");
    //.attr("transform", function(d) { return "translate(0," + y0(y0.domain()[0]) +  ")"; });

selection.selectAll("rect")
    .data(function (d) { return d.values; })
  .enter().append("rect")
    .attr("width", x.rangeBand())
    .attr("x", function(d) { return x(d.period); })
    .attr("y", function (d) { return y(d.amount + d.valueOffset); })
    .attr("height", function (d) { return y(d.valueOffset) - y(d.valueOffset + d.amount); })
    .style("fill", function (d) { return color(d.type); })
    .style("stroke", "grey");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="chart"></div>

Some notes on the above snippet (that match my comments):

  1. A much simpler nest:

    var nest = d3.nest()
                 .key(function(d) { return d["type"];});
    

    This is much simpler than your previous one, and there is no need to do the rollup function. Rollups are generally required when you want to aggregate your data, in this case you don't need to, which should be a giveaway that your nesting was too complex.

  2. The calculation of the maximum value for the y axis:

    var yMax = d3.max(dataByType, function(type) { return d3.max(type.values, function(d) { return d.amount + d.valueOffset; }); });
    

    This will calculate the maximum value that your axis needs to take, making everything fit nicely.

  3. If you look at the resulting SVG, you'll see what I mean about the grouping of the rects in each stack. I generally find that it's easier to group this way. I guess there's no "right" way, but this typically works best for me.

  4. The calculation of the valueOffset in the stack:

    d3.layout.stack()
             .values(function(d) { return d.values; })
             .x(function(d) { return d.period; })
             .y(function(d) { return d.amount; })
             .out(function(d, y0) { 
               d.valueOffset = y0; 
             });
    

    The calculated valueOffset is used to "move" each rect in the stack into position relative to the other rects. You'll see it used a few times, calculating the max y value, the y attr of each rect, and the height of each rect.

I haven't explained every change that I've made, but hopefully with the above and the snippet you'll be able to work through the differences and apply it your exact use case.

Ben Lyall
  • 1,976
  • 1
  • 12
  • 14
  • Thank you for the detailed answer! It is really helpful. I just want to ask about y maximum value. Regarding the calculations,`yMax` value becomes bigger than the maximum amount value in the JSON data array because it is `d.amount + d.valueOffset`. Is it possible to keep it as the max value in the data and lower `rect` to be in front of the higher ones. Thank you! – Ils Oct 15 '14 at 09:58
  • Oh, now I see. If I have the same value for each group types, one of them will be hidden as well. I think I misunderstood the logic in the chart. – Ils Oct 15 '14 at 10:15
  • yMax is only used to set the maximum value that is displayed on the y axis. If you're going to stack the values, then you need to account for the maximum that the stack will result in. In the case of your data, it's 196 (85 + 111). Doing it this way you end up with a bar that corresponds to a value of 85, and then bar with a value of 111 stacked on top. If you're putting one rect in front of another, then you're not really creating a stacked bar chart. – Ben Lyall Oct 15 '14 at 10:15