3

The below code will create a bar chart of a single value within the array of objects called ‘data’. How can I use the collection of data to create a stacked bar chart?

set up the margins

 var w = 700;
 var    h = 500;
 var margin = {
    top: 58,
 bottom: 120,
    left: 60,
    right: 40
};
var width = w - margin.left - margin.right;
var height = h - margin.top - margin.bottom;

define the data

 data =[
 { name: "Subway", carbs: "480", fat: "400", protein: "120", sum: "1000"}, 
 { name: "Steak & Potatos", carbs: "900", fat: "350", protein: "200", sum:    "1450"}, 
 { name: "Sausage", carbs: "50", fat: "350", protein: "80", sum: "480"} 
 ];

I want the height of the bar to be the sum of the values for carbs, fat, protein, but with each value to be of a different color. define x

 var x = d3.scale.ordinal()
    .domain(data.map(function(e){
        return e.name;
    }))
    .rangeBands([0,width]);

define y. This is where I think I need help

 var y = d3.scale.linear()
    .domain([0, d3.max(data, function(d,i){

        return parseInt(d.carbs) ;
    })])
    .range([height, 0]);

The above will give me a bar with a single value (carbs). But what I really want to do is to create a stacked bar chart. Any help will be greatly appreciated.

Added after below posted answer 'z is your color scale here which will decide what your rectangles look like'

 var z = d3.scale.ordinal()
.range(["#ff0", "#f00", "#0ff"])
.domain(["carbs", "fat", "protein"]);

This is the plot function which draws the chart

 function plot(params){

    this.append('g')
    .call(params.gridlines)
    .classed('gridline', true)
    .attr('transform','translate(0,0)');

    this.selectAll('bar')
    .data(params.data)
    .enter()
    .append('rect')
    .classed('bar', true)
    .attr('x', function(d,i){
        return x(d.name)
    })
    .attr('y',function(d,i){
        return y(d.carbs);
    })
    .attr('width', function(d){
        return x.rangeBand();
    })
    .attr('height', function(d,i){
        return height - y(d.carbs)
    })
    .style('fill',function(d,i){
        return ordinal_color_scale(i);
    });

this.selectAll('.bar_label')
        .data(params.data)
        .enter()
            .append('text')
            .classed('bar_label', true)
            .attr('x', function(d,i){
                return x(d.name) + (x.rangeBand()/2);
            })
            .attr('dx', 0)
            .attr('y', function(d,i){
                    return y(d.carbs);
                })
            .attr('dy', -6)
            .text(function(d){
                return d.carbs;
            });

        this.append('g')
            .classed('x axis', true)
            .style('fill', '#ffd000')
            .attr('transform','translate('+ 0 +','+ height +')')
            .call(params.axis.x)
                .selectAll('text')
                .style('text-anchor','end')
                .attr( 'dx', -8)
                .attr('dy',8)
                .attr('transform','translate(0,0) rotate(-45)');

        this.append('g')
            .classed('y axis', true)
            .style('fill', '#ffd000')
            .attr('transform','translate(0,0)')
            .call(params.axis.y);
}

plot.call(chart, {
    data: data,
    axis: {
        x: x_axis,
        y: y_axis
    },
    gridlines: y_gridlines
});

What I don't understand is how to draw the z variable in the bars on the chart.

Derreck
  • 158
  • 9

1 Answers1

1

Your domain for y is indeed what's wrong here. The range needs take all the variables into account, not just carbs. In other words, your max function needs to consider the sum of all properties.

var y = d3.scale.linear()
    .domain([0, d3.max(data, function(d,i){
        return parseInt(d.sum);
})])
.range([height, 0]);

This is because, in the end, your bars will be as high as the sum of all your properties (since it's a stacked bar graph)

You will then need to use the stack function when you actually draw your <rect> elements. Each bar will contain 3 <rect> elements (one for carbs, fat and protein stacked one on top of the other) You didn't post any code for that but it will most likely look like :

// z is your color scale here which will decide what your rectangles look like
var z = d3.scaleOrdinal()
    .range(["#<colorForCarbs>", "#<colorForFat>", "#<colorForProtein>"])
    .domain(["carbs", "fat", "protein"]);

g.selectAll(".serie")
.data(stack.keys(["carbs", "fat", "protein"])(data))
.enter().append("g")
  .attr("class", "serie")
  .attr("fill", function(d) { return z(d.key); })
.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
  .attr(class, 'bar')
  .attr("x", function(d) { return x(d.data.name); })
  .attr("y", function(d) { return y(d[1]); })
  .attr("height", function(d) { return y(d[0]) - y(d[1]); })
  .attr("width", x.bandwidth());

A "serie" in the example is, for example, all the rectangles that are related to "carbs" (as opposed to a stack that would contain the 3 rectangles for carbs, fat and protein).

This example should give you everything you need if you are missing something : http://bl.ocks.org/mbostock/3886208

EDIT

With your updated question, this is the section of the code where you should be using the z scale

this.selectAll('bar')
    .data(params.data)
    .enter()
    .append('rect')
    .classed('bar', true)
    .attr('x', function(d,i){
        return x(d.name)
    })
    .attr('y',function(d,i){
        return y(d.carbs);
    })
    .attr('width', function(d){
        return x.rangeBand();
    })
    .attr('height', function(d,i){
        return height - y(d.carbs)
    })
    .style('fill',function(d,i){
        return ordinal_color_scale(i);
    });

Instead of that, use

var stack = d3.stack(); 
g.selectAll(".serie")
    .data(stack.keys(["carbs", "fat", "protein"])(data))
    .enter().append("g")
    .attr("class", "serie")
    .attr("fill", function(d) { return z(d.key); })
    .selectAll("rect")
       .data(function(d) { return d; })
       .enter()
       .append("rect")
           .attr("x", function(d) { return x(d.data.name); })
           .attr("y", function(d) { return y(d[1]); })
           .attr("height", function(d) { return y(d[0]) - y(d[1]); })
           .attr("width", x.rangeBand());
Mr Lister
  • 45,515
  • 15
  • 108
  • 150
Hugo Migneron
  • 4,867
  • 1
  • 32
  • 52
  • I added the z variable changed it to scale.ordinal because scaleOrdinal was getting this error 'Uncaught TypeError: d3.scaleOrdinal is not a function(…)'. I want to note that I copied the code in the linked example you gave me, and the code worked with its d3 library, but not with the one in my library. That is wierd! – Derreck Nov 10 '16 at 12:23
  • OK, that's because you use d3 v3 (and my code used d3 v4). Shouldn't really change much. See my edit – Hugo Migneron Nov 10 '16 at 14:47
  • When I use d3 v4 and switch scale.ordinal to scaleOrdinal, then I get the error saying: 'd3.scaleOrdinal(...).domain(...).rangeBands is not a function(…)'. Am I getting any closer? – Derreck Nov 10 '16 at 17:07
  • Yes, very close I think ! Use `x.bandwidth()` instead of `x.rangeBand()` – Hugo Migneron Nov 10 '16 at 19:35
  • OK, now I get this error: stack.keys is not a function(…). – Derreck Nov 10 '16 at 20:20
  • Did you add the `var stack = d3.stack();` before the statetement ? – Hugo Migneron Nov 10 '16 at 20:44
  • If I do I get this error: 'd3.stack is not a function(…)'. But if I use d3.layout.stack I get the other error. 'stack.keys is not a function(…)'. Both give me no bars. – Derreck Nov 10 '16 at 21:05
  • Not sure what to tell you. The function definitely exists in d3 v4. http://codepen.io/anon/pen/zorwer?editors=1111 – Hugo Migneron Nov 10 '16 at 21:16
  • I'll check out your codepen links and keep trying and if I get it I'll post here if I figure it out. Thanks!. – Derreck Nov 10 '16 at 21:29