block by harrystevens dea8fdcdae7401ab486bfc7fcc9f6882

Normalized Stacked Bar Chart

Full Screen

Create a normalized stacked bar chart in d3 (without the stack layout).

The CSV file can contain any number of groups and up to 8 categories. It must not have a total row at the bottom; that row is calculated with Javascript.

Libraries: d3.js, underscore.js, jQuery

index.html

<html>
	<head>

		<link rel="stylesheet" href="styles.css" />

	</head>
	<body>

		<div class="legend"></div>
		<div class="chart"></div>

		<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
		<script src="https://d3js.org/d3.v4.min.js"></script>
		<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
		<script src="scripts.js"></script>

	</body>
</html>

data.csv

group,cat_a,cat_b,cat_c,cat_d,cat_e,cat_f,cat_g,cat_h
Andrew,9577,942,471,2355,632,324,123,2355
Matthew,10362,1256,157,314,754,217,235,3611
Mark,9420,157,2669,1099,274,364,123,2355
Luke,9106,314,2512,471,123,127,234,3297
John,7693,157,3768,942,964,876,234,3140
Jimmy,6594,785,3611,1099,227,164,264,3611
Bill,6437,3297,1256,471,532,653,278,4239
Bob,5495,2512,2983,942,346,164,274,3768
Kate,7493,372,4830,1632,274,123,174,1537
Alice,3513,631,145,1473,227,126,237,1573
Sally,8574,2534,537,214,254,763,143,2500
Mary,2583,253,8459,203,126,643,2187,4832
Jenny,2365,2305,2805,234,123,129,347,204
Barbara,1305,3854,2035,2034,854,743,165,123

scripts.js

var barHeight = 25, barPad = 3, textDy = 16, width = 600;

var margin = {top:0,bottom:0,left:60,right:0}

var svg = d3.select(".chart").append("svg")
    .attr("width", width);

var x = d3.scaleLinear()
    .range([0,width - margin.left])
    .domain([0,100]);

var colorArray = ['#66c2a5','#fc8d62','#8da0cb','#e78ac3','#a6d854','#ffd92f','#e5c494','#b3b3b3'];

d3.csv("data.csv",types,function(error,data){

  var keys = [], legKeys = [];

  _.keys(data[0]).forEach(function(d,i){
    if (i > 0){
        d.includes('pct') ? keys.push(d) : legKeys.push(d);
    }
  });

  data = addTotal(data, legKeys);

  // create the legend
  $('.legend').css('margin-left', margin.left);
  legKeys.forEach(function(legKey,i){
    $('.legend').append('<div class="swatch" style="background:' + colorArray[i] + '"></div>' + legKey);
  });

  // sort the data, putting the total at the end
  var sorted = _.sortBy(data,keys[0]).reverse(), total = _.where(sorted,{group:'total'}), rem = _.reject(sorted,{group:'total'}), dat = [];
  rem.forEach(function(r){ dat.push(r); });
  dat.push(total[0]);
  data = dat;

  // now that the data is ready, calculate the height
  var height = (data.length * barHeight) + (barPad * 3);

  d3.select('svg').attr("height", height);

  svg.selectAll('.label')
      .data(data)
    .enter().append('text')
      .attr('class', function(d){
        var label;
        d.group == 'total' ? label = 'label strong' : label = 'label';
        return label;
      })
      .attr('x', margin.left-5)
      .attr('y', function(d,i){ return pad(d,i); })
      .attr('dy', textDy)
      .attr('text-anchor', 'end')
      .text(function(d){ return d.group.charAt(0).toUpperCase() + d.group.slice(1); });

  keys.forEach(function(key,i){

    svg.selectAll('.bar .' + key)
        .data(data)
      .enter().append('rect')
        .attr('class', 'bar ' + key)
        .attr('width', function(d,i){ return x(d[key]); })
        .attr('height', barHeight - barPad)
        .attr('x', function(d){ return widths(keys,key,d); })
        .attr('y', function(d,i){ return pad(d,i); });

    d3.selectAll('.' + key)
      .style('fill', colorArray[i]);

    svg.selectAll('.bar-label .' + key)
        .data(data)
      .enter().append('text')
        .attr('class', 'bar-label ' + key)
        .attr('x', function(d){
          var barX;
          key !== keys[keys.length-1] ? barX = widths(keys,key,d) + 5 : barX = width - 5;
          return barX;
        })
        .attr('dy', textDy)
        .attr('y', function(d,i){ return pad(d,i); })
        .attr('text-anchor', function(d){
          var ta;
          key == keys[keys.length-1] ? ta = 'end' : ta = 'start';
          return ta;
        })
        .text(function(d){
          var text;
          d[key] < 5 ? text = '' : text = Math.round(d[key]) + pct(d, data[0].group, data[data.length-1].group);
          return text;
        });

  });

});

function types(d){

  var keys = _.keys(d);

  var arr = [];
  keys.forEach(function(k,i){
    if (i!==0){
      arr.push(+d[k]);
      d[k] = +d[k];
    }
  });

  var sum = arr.reduce(function(a,b){return a+b}, 0);
  keys.forEach(function(k,i){
    i !== 0 ? d[k + 'pct'] = +d[k]/sum*100 : null;
  });

  return d;
}

function pad(d,i){
  var barI;
  d.group == 'total' ? barI = i * barHeight + (barPad * 2) : barI = i * barHeight;
  return barI;
}

function pct(d, first, last){
  var extra;
  d.group == first || d.group == last ? extra = '%' : extra = '';
  return extra;
}

function widths(keys,key,d){

  if (key == keys[0]) {
    return margin.left;
  } else if (key == keys[1]){
    return margin.left + x(d[keys[0]]);
  } else if (key == keys[2]){
    return margin.left + x(d[keys[0]]) + x(d[keys[1]]);
  } else if (key == keys[3]) {
    return margin.left + x(d[keys[0]]) + x(d[keys[1]]) + x(d[keys[2]]);
  } else if (key == keys[4]) {
    return margin.left + x(d[keys[0]]) + x(d[keys[1]]) + x(d[keys[2]]) + x(d[keys[3]]);
  } else if (key == keys[5]) {
    return margin.left + x(d[keys[0]]) + x(d[keys[1]]) + x(d[keys[2]]) + x(d[keys[3]]) + x(d[keys[4]]);
  }  else if (key == keys[6]) {
    return margin.left + x(d[keys[0]]) + x(d[keys[1]]) + x(d[keys[2]]) + x(d[keys[3]]) + x(d[keys[4]]) + x(d[keys[5]]);
  }  else if (key == keys[7]) {
    return margin.left + x(d[keys[0]]) + x(d[keys[1]]) + x(d[keys[2]]) + x(d[keys[3]]) + x(d[keys[4]]) + x(d[keys[5]]) + x(d[keys[6]]);
  }

}

function addTotal(data,keys){

  var totObj = {};
  var arrA = [];
  keys.forEach(function(key){
    var arrB = [];
    data.forEach(function(d){
      arrB.push(d[key]);
    })
    var sumB = arrB.reduce(function(a,b){ return a+b }, 0);
    totObj[key] = sumB;
    arrA.push(sumB);
  })

  var sumA = arrA.reduce(function(a,b){ return a+b }, 0);

  keys.forEach(function(key){
    totObj[key + 'pct'] = totObj[key] / sumA * 100;
  });

  totObj.group = 'total';

  data.push(totObj);

  return data;
}

styles.css

body {
  font-family: "Helvetica Neue", sans-serif;
  margin: 0 auto;
  display: table;
}
.label {
  font-size: .9em;
}
.label.strong {
  font-weight: 900;
}
.bar-label {
  font-size: .8em;
  fill: #fff;
}
.legend {
  font-size: .9em;
  margin-bottom: 10px;
}
.swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-right: 4px;
  margin-left: 8px;
}
.swatch:first-of-type {
  margin-left: 0px;
}