0
votes

Currently, I have an area graph that displays the areas of several paths of data. However, I'm looking for a way to convert this to a stacked area chart. Basically, I want the datasets to be stacked on top of each other so that they are visible and do not get hidden by other larger areas.

I've found two examples: http://bl.ocks.org/mbostock/3885211 and http://bl.ocks.org/mbostock/3020685. The problem I have is that my data is in a very different format and I'm also unsure of how d3 is actually making this work. Conceptually, I'm having a difficult time understanding what d3 is doing behind the scenes. My understanding is that it is computing the differences between the last layer added but it seems like that could be done without using stacks.

My data is structured like this:

// datasets { data1: Array[10], data2: Array[10], data3: Array[10] }
// data1 [{value: 2340234, year: 1945}...] 

And the drawing function is this:

function draw(series) {
  var data1 = [];
  var data2 = [];
  var data3 = [];
  var prop = 0;

  for (prop in series) {
    var current = series[prop];

    data1.push({value: current.data1, year: current.year});
    data2.push({value: current.data2, year: current.year});
    data3.push({value: current.data3, year: current.year});
  }

  var datasets = {
    data1: data1,
    data2: data2,
    data3: data3
  };

  // updated
  var layers = [datasets.data1, datasets.data2, datasets.data3];

  var height = 800;
  var width = window.innerWidth - 100;

  var x = d3.time.scale()
      .range([0, width]);

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

  var xAxis = d3.svg.axis()
      .scale(x)
      .orient("bottom");

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

  // updated
  var stack = d3.layout.stack()
      .values( function(d) { return d.values; });
      /

  var stacked = stack(layers.map( function(layer) {
    return {
      values: layer.map( function(d) {
        return { year: new Date(d.year, 0), y: +d.value};
      })
    };
  }));

  var area = d3.svg.area()
      .x( function(d) { 
           console.log( x(d.year) + ' is the result of x(d.year)' )
           return x(d.year); 
        })
        // d.year is defined correctly 
      .y0(height)
      .y1( function(d) { 
            console.log( y(d.value) + ' is the result of y(d.value)' )
           return y(d.value); 
         }); 
        // d is Object {year: Sat Jan 01 2005 00:00:00 GMT-0800 (PST), y: 47390331, y0: 0}
        // here y(d.value) returns NaN for some reason

  var svg = d3.select('body').append("svg")
    .append("g")
      .attr("transform", "translate(" + 120 + "," + 30 + ")");

  // Calculate max value
  var maxY = 0;
  var minY = 0;
  series.forEach( function(object) {
    for (var key in object) {
      if (object[key] > maxY) {
        maxY = object[key];
      }
    }
  });

  // Calculate min value
  series.forEach( function(object) {
    for (var key in object) {
      if (object[key] < minY) {
        minY = object[key];
      } else if (minY === 0) {
        continue;
      }
    }
  });

  var minYear = new Date("1945");
  var maxYear = new Date("2015");

  x.domain([minYear, maxYear]);
  y.domain([minY, maxY]);

  svg.append("path").data(stacked)
    .attr('d', function(d) { return area(d.values); });

  svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

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

}

What is the best way to convert this into a stacked area chart?

Edit

Fixed. Solution included calculating area like seen in the example:

var area = d3.svg.area()
    .x(function(d) { return x(d.year); })
    .y0(function(d) { return y(d.y0); })
    .y1(function(d) { return y(d.y0 + d.y); });
1

1 Answers

1
votes

The quick flyby of what you need to do:


Create a suitable stack generator:

var stack = d3.layout.stack()
    .values(function(d) { return d.values; });

Run your data through the stack generator. You should give the generator an array of "layers". For the example below I assume something like layers = [datasets.data1, datasets.data2, datasets.data3] happened.

  var stacked = stack(layers.map(function(layer) {
    return {
      values: layer.map(function(d) {
        return {year: new Date(d.year, 0), y: +d.value};
      })
    };
  }));

The values key is the same one used earlier when setting up the generator. You can add more things to this object if you need them during the drawing later.


After the stacked data has been created, run it through the area generator:

svg.append("path")
  .data(stacked)
  .attr("d", function(d) { return area(d.values); });

In your original code you used .datum(datasets[data]) — you can continue doing this if you want but the for loop will need to run over the stacked data:

for (var data in stacked) { ...

However, I recommend using .data over .datum where possible. You do a lot of manipulation on the original dataset before sending it into the d3 generators but I suspect most of that is unnecessary or duplicated. You should be able to get to something that looks like:

  var datasets = ...

  var stacked = stack(datasets.map(function(dataset) { ... }));

  svg.selectAll("path")
      .data(stacked)
      .enter()
      .attr("d", function(d) { return area(d.values); });

Setting up the datasets and the generators should be nothing more than mapping the locations of the specific fields/values you care about.

If it would help, I can try to get a full working example going. I would need a real data file, though.