2
votes

Reference Mike Bostick's Stacked to Grouped Bar Chart example, I am modifying it to work with a CSV file. I have been working on this for a couple weeks and have looked through countless examples on Stack Overflow and elsewhere and am stumped.

The stacked bar chart works.

Stacked Bar Chart:

Stacked Bar Chart

When I transition to a grouped bar chart I am only having issues referencing the key or series that is stacked or grouped. Right now all the rectangles display on top of each other instead of next to each other.

Grouped Bar Chart:

Grouped Bar Chart

In the function transitionStep2() I want to multiply by a number corresponding to the series or key. I am currently multiplying by the number 1 in this function as a placeholder .attr("x", function(d) { return x(d.data.Year) + x.bandwidth() / 7 * 1; }).

<!DOCTYPE html>
<script src="https://d3js.org/d3.v4.min.js"></script>
<html><body>

<form>
  <label><input type="radio" name="mode" style="margin-left: 10" value="step1" checked>1</label>
  <label><input type="radio" name="mode" style="margin-left: 20" value="step2">2</label>
</form>

<svg id = "bar" width = "500" height = "300"></svg>
<script>
  var svg = d3.select("#bar"),
    margin = {top: 20, right: 20, bottom: 20, left: 20},
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom,
    g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  var x = d3.scaleBand()
    .rangeRound([0, width])
    .padding(0.08);

  var y = d3.scaleLinear()
    .range([height, 0]);

  var color = d3.scaleOrdinal()
    .range(["#7fc97f", "#beaed4", "#fdc086", "#ffff99"]);

  d3.csv("data.csv", function(d, i, columns) {
    for (i = 1, t = 0; i < columns.length; ++i) t += d[columns[i]] = +d[columns[i]];
    d.total = t;
    return d;
  }, function(error, data) {
    if (error) throw error;

  var keys = data.columns.slice(1);

  x.domain(data.map(function(d) { return d.Year; }));
  y.domain([0, d3.max(data, function(d) { return d.total; })]).nice();
  color.domain(keys);

  g.append("g")
    .selectAll("g")
    .data(d3.stack().keys(keys)(data))
    .enter().append("g")
      .attr("fill", function(d) { return color(d.key); })
    .selectAll("rect")
    .data(function(d) { return d; })
    .enter().append("rect")
      .attr("x", function(d) { return x(d.data.Year); })
      .attr("y", function(d) { return y(d[1]); })
      .attr("height", function(d) { return y(d[0]) - y(d[1]); })
      .attr("width", x.bandwidth());

  rect = g.selectAll("rect");
});

  d3.selectAll("input")
    .on("change", changed);

  function changed() {
    if (this.value === "step1") transitionStep1();
    else if (this.value === "step2") transitionStep2();
  }

  function transitionStep1() {
    rect.transition()
      .attr("y", function(d) { return y(d[1]); })
      .attr("x", function(d) { return x(d.data.Year); })
      .attr("width", x.bandwidth())
      .attr("stroke", "green");
  }

  function transitionStep2() {
    rect.transition()
      .attr("x", function(d) { return x(d.data.Year) + x.bandwidth() / 7 * 1; })
      .attr("width", x.bandwidth() / 7)
      .attr("y", function(d) { return y(d[1] - d[0]); })
      .attr("stroke", "red");
  }
</script></body></html>

And the csv file:

Year,A,B,C,D
1995,60,47,28,39
1996,29,56,99,0
1997,30,26,63,33
1998,37,16,48,0
1999,46,49,64,21
2000,78,88,81,57
2001,18,11,11,64
2002,91,76,79,64
2003,30,99,96,79
3

3 Answers

0
votes

The example you're referring has linear values to group the chart into and so .attr("x", function(d, i) { return x(i) + x.bandwidth() / n * this.parentNode.__data__.key; }) works fine.

In your case, the columns/keys is not a linear scale but an ordinal value set that you have to set a scale for:

Refer simple d3 grouped bar chart

To do that i.e. set up a ordinal scale, here's what can be done:

var x1 = d3.scaleBand();
x1.domain(keys).rangeRound([0, x.bandwidth()]);

So this new scale would have a range of the x scale's bandwidth with the domain of ["A", "B", "C", "D"]. Using this scale to set the x attribute of the rects to group them:

.attr("x", function(d) { 
  return x(d.data.Year) + x1(d3.select(this.parentNode).datum().key); 
})

where d3.select(this.parentNode).datum().key represent the column name.

Here's JSFIDDLE (I've used d3.csvParse to parse the data but I'm sure you'll get the point here. It's just the x attribute you need to reset.

Here's a Plunkr that uses a file.

Here's a code snippet as well:

  var svg = d3.select("#bar"),
    margin = {top: 20, right: 20, bottom: 20, left: 20},
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom,
    g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  var x = d3.scaleBand()
    .rangeRound([0, width])
    .padding(0.08);

	var x1 = d3.scaleBand();
  
  var y = d3.scaleLinear()
    .range([height, 0]);

  var color = d3.scaleOrdinal()
    .range(["#7fc97f", "#beaed4", "#fdc086", "#ffff99"]);

var csv = 'Year,A,B,C,D\n1995,60,47,28,39\n1996,29,56,99,0\n1997,30,26,63,33\n1998,37,16,48,0\n1999,46,49,64,21\n2000,78,88,81,57\n2001,18,11,11,64\n2002,91,76,79,64\n2003,30,99,96,79';

var data = d3.csvParse(csv), columns = ["A", "B", "C", "D"];

data.forEach(function(d) {
    for (i = 1, t = 0; i < columns.length; ++i) t += d[columns[i]] = +d[columns[i]];
    d.total = t;
});

  var keys = columns;

  x.domain(data.map(function(d) { return d.Year; }));
  x1.domain(keys).rangeRound([0, x.bandwidth()]);
  y.domain([0, d3.max(data, function(d) { return d.total; })]).nice();
  color.domain(keys);

  g.append("g")
    .selectAll("g")
    .data(d3.stack().keys(keys)(data))
    .enter().append("g")
      .attr("fill", function(d) { return color(d.key); })
    .selectAll("rect")
    .data(function(d) { return d; })
    .enter().append("rect")
      .attr("x", function(d) { return x(d.data.Year); })
      .attr("y", function(d) { return y(d[1]); })
      .attr("height", function(d) { return y(d[0]) - y(d[1]); })
      .attr("width", x.bandwidth());

  rect = g.selectAll("rect");

  d3.selectAll("input")
    .on("change", changed);

  function changed() {
    if (this.value === "step1") transitionStep1();
    else if (this.value === "step2") transitionStep2();
  }

  function transitionStep1() {
    rect.transition()
      .attr("y", function(d) { return y(d[1]); })
      .attr("x", function(d) { return x(d.data.Year); })
      .attr("width", x.bandwidth())
      .attr("stroke", "green");
  }
  
	function transitionStep2() {
    rect.transition()
      .attr("x", function(d) { 
      	return x(d.data.Year) + x1(d3.select(this.parentNode).datum().key); 
    	})
      .attr("width", x.bandwidth() / 7)
      .attr("y", function(d) { return y(d[1] - d[0]); })
      .attr("stroke", "red");
  }
<!DOCTYPE html>
<script src="https://d3js.org/d3.v4.min.js"></script>
<html><body>

<form>
  <label><input type="radio" name="mode" style="margin-left: 10" value="step1" checked>1</label>
  <label><input type="radio" name="mode" style="margin-left: 20" value="step2">2</label>
</form>

<svg id = "bar" width = "500" height = "300"></svg>

Hope something helps. :)

0
votes

I updated your examples so that it works, see https://jsfiddle.net/mc5wdL6s/84/

  function transitionStep2() {
    rect.transition()
     .duration(5500)
      .attr("x", function(d,i) { 
        console.log("d",d);
        console.log("i",i);
      return x(d.data.Year) + x.bandwidth() / m / n * i; })
      .attr("width", x.bandwidth() / n)
      .attr("y", function(d) { return y(d[1] - d[0]); })
      .attr("height", function(d) { return y(0) - y(d[1] - d[0]); })
      .attr("stroke", "red");
  }
0
votes

You have to pass the index of the key to the individual rectangles and use this index to multiply with the reduced bandwith. If you also use the length of the keys array (instead of 7) you are CSV column count independent. You need to put the declaration of keys variable outside the d3.csv handler.

You forgot to stroke the initial rects green.

<script>
var svg = d3.select("#bar"),
    margin = {top: 20, right: 20, bottom: 20, left: 20},
    width = +svg.attr("width") - margin.left - margin.right,
    height = +svg.attr("height") - margin.top - margin.bottom,
    g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var x = d3.scaleBand()
    .rangeRound([0, width])
    .padding(0.08);

var y = d3.scaleLinear()
    .range([height, 0]);

var color = d3.scaleOrdinal()
    .range(["#7fc97f", "#beaed4", "#fdc086", "#ffff99"]);

var keys;

d3.csv("/data.csv", function(d, i, columns) {
    for (i = 1, t = 0; i < columns.length; ++i) t += d[columns[i]] = +d[columns[i]];
    d.total = t;
    return d;
}, function(error, data) {
    if (error) throw error;

    keys = data.columns.slice(1);

    x.domain(data.map(function(d) { return d.Year; }));
    y.domain([0, d3.max(data, function(d) { return d.total; })]).nice();
    color.domain(keys);

    var stackData = d3.stack().keys(keys)(data);
    stackData.forEach(element => {
        var keyIdx = keys.findIndex(e => e === element.key);
        element.forEach(e2 => { e2.keyIdx = keyIdx; });
    });

    g.append("g")
    .selectAll("g")
    .data(stackData)
    .enter().append("g")
        .attr("fill", function(d) { return color(d.key); })
    .selectAll("rect")
    .data(function(d) { return d; })
    .enter().append("rect")
        .attr("x", function(d) { return x(d.data.Year); })
        .attr("y", function(d) { return y(d[1]); })
        .attr("height", function(d) { return y(d[0]) - y(d[1]); })
        .attr("width", x.bandwidth())
        .attr("stroke", "green");

    rect = g.selectAll("rect");
});

d3.selectAll("input")
    .on("change", changed);

function changed() {
    if (this.value === "step1") transitionStep1();
    else if (this.value === "step2") transitionStep2();
}

function transitionStep1() {
    rect.transition()
    .attr("y", function(d) { return y(d[1]); })
    .attr("x", function(d) { return x(d.data.Year); })
    .attr("width", x.bandwidth())
    .attr("stroke", "green");
}

function transitionStep2() {
    rect.transition()
    .attr("x", function(d, i) { return x(d.data.Year) + x.bandwidth() / (keys.length+1) * d.keyIdx; })
    .attr("width", x.bandwidth() / (keys.length+1))
    .attr("y", function(d) { return y(d[1] - d[0]); })
    .attr("stroke", "red");
}
</script>