Search code examples
javascriptjsond3.jsstackedbarseries

Stacked bar chart in D3js - bars are not on the correct places


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?


Solution

  • 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.