Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Making a grouped bar chart when my groups are varied sizes?

Tags:

d3.js

I am trying to make a bar chart out of some grouped data. This is dummy data, but the structure is basically the same. The data: election results includes a bunch of candidates, organized into the districts they were running in, and the total vote count:

district,candidate,votes
Dist 1,Leticia Putte,3580
Dist 2,David Barron,1620
Dist 2,John Higginson,339
Dist 2,Walter Bannister,2866
[...]

I'd like to create a bar or column chart (either, honestly, though my end goal is horizontal) that groups the candidates by district.

Mike Bostock has an excellent demo but I'm having trouble translating it intelligently for my purposes. I started to tease it out at https://jsfiddle.net/97ur6cwt/6/ but my data is organized somewhat differently -- instead of rows, by group, I have a column that sets the category. And there might be just one candidate or there might be a few candidates.

Can I group items if the groups aren't the same size?

like image 285
Amanda Avatar asked Dec 25 '22 06:12

Amanda


1 Answers

My answer is similar to @GerardoFurtado but instead I use a d3.nest to build a domain per district. This removes the need for hardcoding values and cleans it up a bit:

y0.domain(data.map(function(d) { return d.district; }));

var districtD = d3.nest()
  .key(function(d) { return d.district; })
  .rollup(function(d){
    return d3.scale.ordinal()
        .domain(d.map(function(c){return c.candidate}))
      .rangeRoundBands([0, y0.rangeBand()], pad);
  }).map(data);

districtD becomes a map of domains for your y-axis which you use when placing the rects:

  svg.selectAll("bar")
      .data(data)
      .enter().append("rect")
      .style("fill", function(d,i) {
          return color(d.district);
      })
      .attr("x", 0)
      .attr("y", function(d) { return y0(d.district) + districtD[d.district](d.candidate); })
      .attr("height", function(d){
        return districtD[d.district].rangeBand();
      })
      .attr("width", function(d) {
        return x(d.votes);
      });

I'm off to a meeting but the next step is to clean up the axis and get the candidate names on there.


Full running code:

var url = "https://gist.githubusercontent.com/amandabee/edf73bc0bbe131435c952f5ed47524a6/raw/99febb9971f76e36af06f1b99913fcaa645ecb3e/election.csv"
var m = {top: 10, right: 10, bottom: 50, left: 110},
  w = 800 - m.left - m.right,
  h = 500 - m.top - m.bottom,
  pad = .1;

var x = d3.scale.linear().range([0, w]);
y0 = d3.scale.ordinal().rangeRoundBands([0, h], pad);

var color = d3.scale.category20c();

var yAxis = d3.svg.axis()
    .scale(y0)
    .orient("left");

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom")
    .ticks(5)
    .tickFormat(d3.format("$,.0f"));


var svg = d3.select("#chart").append("svg")
  .attr("width", w + m.right + m.left + 100)
  .attr("height", h + m.top + m.bottom)
  .append("g")
  .attr("transform",
        "translate(" + m.left + "," + m.top + ")");

        // This moves the SVG over by m.left(110)
        // and down by m.top (10)


  d3.csv(url, function(error, data) {

    data.forEach(function(d) {
      d.votes = +d.votes;
    });
    
    y0.domain(data.map(function(d) { return d.district; }));
    districtD = d3.nest()
    	.key(function(d) { return d.district; })
      .rollup(function(d){
      	console.log(d);
        return d3.scale.ordinal()
        	.domain(d.map(function(c){return c.candidate}))
          .rangeRoundBands([0, y0.rangeBand()], pad);
      })
      .map(data);    
		
    x.domain([0, d3.max(data, function(d) {
        return d.votes;
      })]);

      svg.append("g")
          .attr("class", "x axis")
          .attr("transform", "translate(0," + h + ")")
          .call(xAxis)
          .selectAll("text")
          .style("text-anchor", "middle");

      svg.append("g")
          .attr("class", "y axis")
          .call(yAxis)
          .append("text");

      svg.selectAll("bar")
          .data(data)
          .enter().append("rect")
          .style("fill", function(d,i) {
              return color(d.district);
          })
          .attr("x", 0)
          .attr("y", function(d) { return y0(d.district) + districtD[d.district](d.candidate); })
          .attr("height", function(d){
          	return districtD[d.district].rangeBand();
          })
          .attr("width", function(d) {
            return x(d.votes);
            });

      svg.selectAll(".label")
  			   .data(data)
  			   .enter().append("text")
  			   .text(function(d) {
             return (d.votes);
             })
  			   .attr("text-anchor", "start")
           .attr("x", function(d) { return x(d.votes)})
           .attr("y", function(d) { return y0(d.district) +  districtD[d.district](d.candidate) + districtD[d.district].rangeBand()/2;})
  			   .attr("class", "axis");

  });
    .axis {
      font: 10px sans-serif;
    }
    .axis path, .axis line {
      fill: none;
      stroke: black;
      shape-rendering: crispEdges;
    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="chart"></div>

An alternate version which sizes the bars the same and scales the outer domain appropriately:

<!DOCTYPE html>
<html>

<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
  <style>
    .label {
      font: 10px sans-serif;
    }
    
    .axis {
      font: 11px sans-serif;
      font-weight: bold;
    }
    
    .axis path,
    .axis line {
      fill: none;
      stroke: black;
      shape-rendering: crispEdges;
    }
  </style>
</head>

<body>
  <div id="chart"></div>
  <script>
    var url = "https://gist.githubusercontent.com/amandabee/edf73bc0bbe131435c952f5ed47524a6/raw/99febb9971f76e36af06f1b99913fcaa645ecb3e/election.csv"
    var m = {
        top: 10,
        right: 10,
        bottom: 50,
        left: 110
      },
      w = 800 - m.left - m.right,
      h = 500 - m.top - m.bottom,
      pad = .1, padPixel = 5;

    var x = d3.scale.linear().range([0, w]);
    var y0 = d3.scale.ordinal();

    var color = d3.scale.category20c();

    var yAxis = d3.svg.axis()
      .scale(y0)
      .orient("left");

    var xAxis = d3.svg.axis()
      .scale(x)
      .orient("bottom")
      .ticks(5)
      .tickFormat(d3.format("$,.0f"));


    var svg = d3.select("#chart").append("svg")
      .attr("width", w + m.right + m.left + 100)
      .attr("height", h + m.top + m.bottom)
      .append("g")
      .attr("transform",
        "translate(" + m.left + "," + m.top + ")");

    // This moves the SVG over by m.left(110)
    // and down by m.top (10)


    d3.csv(url, function(error, data) {

      data.forEach(function(d) {
        d.votes = +d.votes;
      });

      var barHeight = h / data.length;

      y0.domain(data.map(function(d) {
        return d.district;
      }));
      
      var y0Range = [0];
      districtD = d3.nest()
        .key(function(d) {
          return d.district;
        })
        .rollup(function(d) {
          var barSpace = (barHeight * d.length);
          y0Range.push(y0Range[y0Range.length - 1] + barSpace);
          return d3.scale.ordinal()
            .domain(d.map(function(c) {
              return c.candidate
            }))
            .rangeRoundBands([0, barSpace], pad);
        })
        .map(data);
      
      y0.range(y0Range);
      
      x.domain([0, d3.max(data, function(d) {
        return d.votes;
      })]);

      svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + h + ")")
        .call(xAxis)
        .selectAll("text")
        .style("text-anchor", "middle");

      svg.append("g")
        .attr("class", "y axis")
        .call(yAxis)
        .append("text");

      svg.selectAll("bar")
        .data(data)
        .enter().append("rect")
        .style("fill", function(d, i) {
          return color(d.district);
        })
        .attr("x", 0)
        .attr("y", function(d) {
          return y0(d.district) + districtD[d.district](d.candidate);
        })
        .attr("height", function(d) {
          return districtD[d.district].rangeBand();
        })
        .attr("width", function(d) {
          return x(d.votes);
        });

      var ls = svg.selectAll(".labels")
        .data(data)
        .enter().append("g");
        
      ls.append("text")
        .text(function(d) {
          return (d.votes);
        })
        .attr("text-anchor", "start")
        .attr("x", function(d) {
          return x(d.votes)
        })
        .attr("y", function(d) {
          return y0(d.district) + districtD[d.district](d.candidate) + districtD[d.district].rangeBand() / 2;
        })
        .attr("class", "label");

      ls.append("text")
        .text(function(d) {
          return (d.candidate);
        })
        .attr("text-anchor", "end")
        .attr("x", -2)
        .attr("y", function(d) {
          return y0(d.district) + districtD[d.district](d.candidate) + districtD[d.district].rangeBand() / 2;
        })
        .style("alignment-baseline", "middle")
        .attr("class", "label");

    });
  </script>
</body>

</html>
like image 50
Mark Avatar answered Feb 22 '23 03:02

Mark