1

I'm looking for some pointers on how I can fix how my data is being visualized as a bar chart. I've created a small multiples visualization of data that appears over time. Because of the nature of the way the data was collected, the data shares certain attributes. By way of example, you can see an excerpt of the .csv below where group and date are repeated:

group,date,totals
acid,1641,8438
acid,1641,0
acid,1641,0
beef,1641,977.85
beef,1641,0
beef,1641,0
beef,1641,164
beef,1641,5.25
bird,1641,121
bird,1641,0
bird,1641,12
bird,1641,1558
bird,1641,729
bird,1641,1680
bird,1641,0
bird,1641,343
bird,1641,3
bird,1641,2092
bird,1641,0
bird,1641,0
bird,1641,107
bird,1641,2679
bird,1641,167
bird,1641,649
boar,1641,41
boar,1641,13
cheese,1641,13
cheese,1641,22
cheese,1641,1071
cheese,1641,17195

(You can see the entire dataset here.)

The problem I'm running in to is wherever a date is shared, instead of summing the totals column it stacks the data on top of one another. For example, see:

Bar charts

What I'm struggling with is having the data displayed as a single bar chart. (I have a secondary problem with the y-axis not scaling correctly). I've tried to use nest to fix the problem of the years (this worked for groups) but I can't seem to get it with date.

Here's my code, thus far:

var margin = {top: 20, right: 30, bottom: 30, left: 50},
    width = 400 - margin.right - margin.left,
    height = 150 - margin.top - margin.bottom,
    parse = d3.time.format("%Y").parse;

// scales
var x = d3.time.scale().range([0, width]),
    y = d3.scale.linear().range([0, height]),
    yscaled = d3.scale.linear().range([height, 0]);

// load data
d3.csv("revised.csv", function(error, data) {

  // nest values by group
  var groups = d3.nest()
      .key(function(d) { return d.group; })
      .entries(data);

  // parse dates and numbers, assume values are sorted by date
  // compute the maximum totals per group
  groups.forEach(function(s) {
    s.values.forEach(function(d) { d.date = parse(d.date); d.totals = +d.totals; });
    s.maxtotals = d3.max(s.values, function(d) { return d.totals; });
  });

  // compute the minimum and maximum date across groups
  x.domain([
    d3.min(groups, function(s) { return s.values[0].date; }),
    d3.max(groups, function(s) { return s.values[s.values.length - 1].date; })
  ]);
  // y.domain([0, d3.max(data, function(d) { return d.totals; })]);

  // add an SVG element for each group
  var svg = d3.select("body").selectAll("g")
      .data(groups)
    .enter().append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom);

  var canvas_g = svg.append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      .each(function(d) {
          var g = d3.select(this);
          g.append("g");
        // .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

        y.domain([0, d3.max(d.values, function(r) { return r.totals; })]);
        yscaled.domain([0, d3.max(d.values, function(r) { return r.totals; })]);

      // Draw the charts.

        g.selectAll("rect.aevents")
          .data(d.values)
        .enter().append("rect")
          .attr("height", function(p) {return y(p.totals)})
          .attr("width", 5)
          .attr("x", function(p, q) {return x(p.date)})
          .attr("y", function(p) {return height - y(p.totals)})
          // .on("click", function(p) {console.log(p.date)})
          .attr("class", "bar");
      });

  // draw x axis
  canvas_g.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.svg.axis().scale(x).orient("bottom"));

  // draw y axis
  canvas_g.append("g")
      .attr("class", "y axis")
      .call(d3.svg.axis().scale(yscaled).orient("left"));

  // add a small label for the group name
  svg.append("text")
      .attr("x", width + 40)
      .attr("y", height + 15)
      .attr("text-anchor", "end")
      .text(function(d) { return d.key; });

});

Is there a different way I should loop through the data? Or, do I need to restructure the data somehow? Thank you in advance!

Jason Heppler
  • 706
  • 9
  • 29
  • How do you want to represent bars when they collide on a date? do you want them to have all the same size and go to the right, with the risk of going over future date data or do you want to do something else? The problem with this visualization is that you use the x coordinate as another scale than time. There might be a better visualization for this usage. – Christopher Chiche Apr 16 '13 at 02:29
  • Where data collides on a date, I want the data to be totaled together. So in the example above for `beef`, rather than three bars stacked on top of one another for the year 1641 it'll instead return `1147.1`. – Jason Heppler Apr 16 '13 at 03:47
  • Can you please setup a jsfiddle? It would make it easier... – Christopher Chiche Apr 16 '13 at 03:54

1 Answers1

3

First you can read this answer that will help you if you want to know more about d3.nest().

For your problem, a good method can be to first nest the elements using d3.nest() and then to map the groups to their desired form. Which gives:

function formatData(inputData){ 
    var array = d3.nest()
        .key(function(d){return d.group})
        .key(function(d){return d.date})
        .entries(inputData)
        .map(function(d){
            var group = d.key
            var values = d.values.map(function(dd){
                var date = dd.key
                var total = 0
                dd.values.forEach(function(ddd){
                    total = total + ddd.totals        
                })
                return {date:date, totals:total}
            })
            return {'group':group, 'values':values}
        })
    obj = {}
    array.forEach(function(d){
        obj[d.group] = d.values
    })
    return obj
}

with inputData being the data loaded using d3.csv(). This way you can get the best of nest() and `map()``

If you want to draw the bar chart only for beef you can now do:

var beefData = formatData(myInput).beef

jsFiddle: http://jsfiddle.net/chrisJamesC/FJmMD/3/

Community
  • 1
  • 1
Christopher Chiche
  • 15,075
  • 9
  • 59
  • 98
  • Thanks for the advice! I think I'm getting closer, but I'm getting weird data returned. It's more or less accurate, but I'm getting zeros appended and prepended to the `totals`, like this: {"acid":[{"date":"1641","totals":"0843800"},{"date":"1644","totals":"0435200"},{"date":"1645","totals":"0551800"},{"date":"1648","totals":"0700"},{"date":"1656","totals":"0000"},{"date":"1657","totals":"0000"},{"date":"1659","totals":"04800"},{"date":"1662","totals":"0000"},{"date":"1663","totals":"01205300"},{"date":"1664","totals":"0192000"},{"date":"1665","totals":"0000"} – Jason Heppler Apr 16 '13 at 18:38
  • Is there an issue with the way I'm accessing the CSV? When I run everything with your example dataset, it seems to work fine: http://jsfiddle.net/hepplerj/7jgqW/. – Jason Heppler Apr 16 '13 at 18:44
  • 1
    The problem is that you don't convert the strings to ints. Just replace the line where I compute the total by: `total = total + parseFloat(ddd.totals)`. Here is the corresponding [jsFiddle](http://jsfiddle.net/chrisJamesC/7jgqW/2/) – Christopher Chiche Apr 16 '13 at 18:59