Skip to content
Advertisement

D3 Stacked Bar Chart: Bars Overlap

I’m building an interactive bar chart with D3 (version 4). Users may choose a single category (this works fine as coded below) or they can view all categories, in which case the color-coded bars should be stacked. The issue is that I’m not sure how to manipulate the y-coordinates so that each bar is placed above another when the user has chosen All Categories. Right now they overlap because they all start from y:0.

Here’s what it looks like (each stack would add up to +/- 200 to ~280 if not overlapped):

stacked bar chart, minus the stacking

And here’s my function for displaying the chart:

function doChart(chart_json, category, years){
    console.log("loading data for " + (category ? category : 'ALL CATEGORIES')  + "...")
    
    /*
    ****************
    Prepare the data
    ****************    
    */
        
    if(category){
        var filter = chart_json.filter(obj =>{ if(obj.category === category) return obj });
        var max_count = d3.max(filter[0].counts)
        var data = [filter[0]]; // array of one category
    }else{
        var max_count = d3.max(chart_json.totals)
        var data = chart_json // array of all categories 
    }
    
    /*
    **********************
    Set up the chart scale
    **********************  
    */
        
    // D3 margin convention  
    var margin = {top: 20, right: 30, bottom: 20, left: 50},
        height = 400 - margin.top - margin.bottom,
        width = parseInt(d3.select(chart_container).style('width'), 10),
        width = width - margin.left - margin.right
    
    // X scale uses years
    var xScale = d3.scaleBand()
        .domain(years) 
        .range([10, width-10])
        .padding(.2)
    
    // Y scale uses category counts
    var yScale = d3.scaleLinear()
        .domain([0, max_count]) 
        .range([height, 0]);  
                        
    // Add the SVG to the container
    var svg = d3.select(chart_container).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 + ")");
    
    // Append the x axis
    svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(d3.axisBottom(xScale)
            .tickValues(years)
            .tickFormat(d3.format("Y"))); // no commas; format as years
    
    // Append the y axis
    svg.append("g")
        .attr("class", "y axis")
        .call(d3.axisLeft(yScale).ticks( Math.min(10, yScale.domain()[1] ))) // no more ticks than there are total (max 10)
        
    /*
    *****************
    Add the data bars
    *****************
    */

    var bars = svg.append("g").attr("id","bars")
    years.forEach((year,i)=>{
        var bar = bars.append("g").attr("id","bars_"+year)
        var year_index = i;
        bar.selectAll("bar")
          .data(data) 
          .enter()
          .append("rect")
            .attr("x", xScale(year) )
            .attr("y", (d) => {
                return yScale(d.counts[year_index]) 
               })
            .attr("category", (d) =>{
                return d.category
            })
            .attr("year", year)
            .attr("total", (d) => {
                return yScale(d.counts[year_index]) 
               })
            .attr("class", "rect_"+year)
            .attr("width", xScale.bandwidth())
            .attr("height", (d) =>{
                return height - yScale(d.counts[year_index])
            })
            .attr("fill", (d) =>{
                return getMarkerColor(d.category)
            })        

    })
    
}

Here’s some sample data (chart_json for All Categories):

    [{
    "category": "CLUBS AND ORGANIZATIONS",
    "counts": [
      1,
      4,
      4,
      1,
      7,
      3,
      2,
      7
    ]
  },
  {
    "category": "ENTERTAINMENT",
    "counts": [
      4,
      4,
      5,
      3,
      1,
      2,
      1,
      7
    ]
  },
  {
    "category": "FINANCE, INSURANCE AND REAL ESTATE",
    "counts": [
      18,
      14,
      12,
      10,
      17,
      15,
      7,
      9
    ]
  },
  {
    "category": "FOOD SERVICES AND DRINKING PLACES",
    "counts": [
      19,
      14,
      20,
      17,
      11,
      13,
      14,
      64
    ]
  },
  {
    "category": "HOTELS AND MOTELS",
    "counts": [
      1,
      null,
      1,
      1,
      null,
      null,
      1,
      4
    ]
  },
  {
    "category": "INSTITUTION",
    "counts": [
      null,
      null,
      null,
      null,
      3,
      2,
      null,
      19
    ]
  },
  {
    "category": "MANUFACTURER",
    "counts": [
      1,
      1,
      null,
      1,
      4,
      null,
      null,
      null
    ]
  },
  {
    "category": "MUSEUMS AND GALLERIES",
    "counts": [
      null,
      null,
      null,
      null,
      null,
      null,
      null,
      3
    ]
  },
  {
    "category": "PROFESSIONAL",
    "counts": [
      20,
      13,
      17,
      18,
      26,
      10,
      10,
      2
    ]
  },
  {
    "category": "RETAIL",
    "counts": [
      139,
      122,
      128,
      135,
      122,
      105,
      55,
      38
    ]
  },
  {
    "category": "SERVICE",
    "counts": [
      23,
      19,
      28,
      19,
      38,
      22,
      25,
      17
    ]
  },
  {
    "category": "VACANT",
    "counts": [
      18,
      23,
      7,
      8,
      32,
      17,
      null,
      35
    ]
  },
  {
    "category": "OTHER",
    "counts": [
      9,
      10,
      11,
      17,
      27,
      13,
      8,
      28
    ]
  }]

Advertisement

Answer

Answering my own question here. I set a variable y0 to zero and, for each data point incremented by it the count for that data point before passing it to my yScale() function. Previously I was just passing the count itself.

var bars = svg.append("g").attr("id","bars")
years.forEach((year,i)=>{
    var bar = bars.append("g").attr("id","bars_"+year)
    var year_index = i;
    var y0 = 0; // stacking
    bar.selectAll("bar")
      .data(data) 
      .enter()
      .append("rect")
        .attr("x", xScale(year) )
        .attr("y", (d) => {
            var count = d.counts[year_index] ? d.counts[year_index] : 0
            y0 += count // stacking
            return yScale(y0)
           })
        .attr("category", (d) =>{
            return d.category
        })
        .attr("year", year)
        .attr("total", (d) => {
            return d.counts[year_index] ? d.counts[year_index] : 0
           })
        .attr("class", "rect_"+year)
        .attr("width", xScale.bandwidth())
        .attr("height", (d) =>{
            var count = d.counts[year_index] ? d.counts[year_index] : 0
            var bar_height = count ? (height - yScale(count)) : 0
            return bar_height
        })
        .attr("fill", (d) =>{
            return getMarkerColor(d.category)
        })        

})

This basically solved the problem, though I still need to properly sort the data, but that is a separate issue.

enter image description here

User contributions licensed under: CC BY-SA
5 People found this is helpful
Advertisement