Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript summing arrays, using d3.nest()

If I had data that looked like this:

harvest = [{type: "apple", color: "green", value: 1}, 
           {type: "apple", color: "red", value: 2}, 
           {type: "grape", color: "green", value: 3},
           {type: "grape", color: "red", value: 4 }]

I could sum it by various attributes using d3's nest.rollup() function:

sum_by = "color";

rollup = d3.nest().key(function(d) {
  return d[sum_by];
}).rollup(function(d) {
  return d3.sum(d, function(g) {
    return g.value;
  });
}).entries(harvest);

Giving me this:

rollup = [{key: "green", values: 4},
          {key: "red", values: 6}]

Which is just what I want.

However the values in my data consist of arrays, all of equal length:

harvest = [{type: "apple", color: "green", values: [1,2,3,4]}, 
           {type: "apple", color: "red", values: [5,6,7,8]}, 
           {type: "grape", color: "green", values: [9,10,11,12]},
           {type: "grape", color: "red", values: [13,14,15,16] }]

Is it possible to combine these in a similar way? To give for example:

rollup = [{key: "green", values: [10,12,14,16]},
          {key: "red", values: [18,20,22,24]}]

I feel this is probably possible using a d3 rollup function (but it doesn't necessarily have to be done using d3).

RESOLUTION

Thanks to the efforts of @meetamit and @Superboggly I have three solutions:

Version 1 (preferred because it uses reduce() just once and map() just once):

function sumArrays(group) {
  return group.reduce(function(prev, cur, index, arr) {
    return {
      values: prev.values.map(function(d, i) {
        return d + cur.values[i];
      })
    };
  });
}

Version 2:

function sumArrays(group) {
  return group.map(function(h) {
    return h.values;
  }).reduce(function(prev, cur, index, arr) {
    return prev.map(function(d, i) {
      return d + cur[i];
    });
  });
}

Version 3 (for interest because array length can vary):

function sumArrays(group) {
  return group.reduce(function(prev, cur, index, arr) {
    return prev.map(function(d, i) {
      return d + cur.values[i];
    });
  }, [0, 0, 0, 0]);
}

Called like this:

function rollupArrays() {
  return d3.nest().key(function(d) {
    return d[sum_by];
  }).rollup(sumArrays).entries(harvest);
}

And converted to CoffeeScript:

rollupArrays = ->
  d3.nest().key (d) ->
    d[sum_by]
  .rollup(sumArrays).entries(harvest)

sumArrays = (group) ->
  group.reduce (prev, cur, index, arr) ->
    values: prev.values.map (d,i) ->
      d + cur.values[i]

UPDATE

This method isn't suitable if the function must run, even with one input row. See Part II

like image 605
Derek Hill Avatar asked Nov 14 '12 13:11

Derek Hill


3 Answers

One solution uses [].reduce() and [].map():

// eg: sumArrays([ [1,2,3,4], [5,6,7,8] ]);// <- outputs [6, 8, 10, 12]
function sumArrays(arrays) {
  return arrays.reduce(
    function(memo, nums, i) {
      if(i == 0)
        return nums.concat();
      else
        return memo.map(
          function(memoNum, i) {
            return memoNum + nums[i];
          }
        );
    },
    [ ]// Start with empty Array for memo
  );
}

Both reduce and map are not native in old JS, so best use a module (underscore, or maybe there's a d3 equivalent to reduce, but I haven't seen it).

EDIT

Using it in your code:

sum_by = "color";

rollup = d3.nest().key(function(d) {
  return d[sum_by];
}).rollup(function(d) {
  var arraysToSum = d.map(function(g) { return g.values; });
  return sumArrays(arraysToSum)
}).entries(harvest);
like image 95
meetamit Avatar answered Oct 13 '22 00:10

meetamit


@meetamit I like your idea of using reduce.

If you want to solve this just using d3 it also has a built in reduce which you can use in conjunction with the nest function:

var rollup = d3.nest().key(function(d) {
  return d[sum_by];
}).rollup(function(d) {
    var result = d.reduce(function(prev, cur, index, arr) {
        return prev.values.map(function(d,i) { return d + cur.values[i];});
    });

    return result;
}).entries(harvest);

If you want you can play with it here.

like image 29
Superboggly Avatar answered Oct 13 '22 00:10

Superboggly


This solution makes use of d3.rollups and d3.transpose, new features of d3.array:

var input = [
  { type: "apple", color: "green", values: [1, 2, 3, 4] },
  { type: "apple", color: "red",   values: [5, 6, 7, 8] },
  { type: "grape", color: "green", values: [9, 10, 11, 12] },
  { type: "grape", color: "red",   values: [13, 14, 15, 16] }
];

var rolled_up = d3.rollups(
  input,
  vs => d3.transpose(vs.map(d => d.values)).map(vs => d3.sum(vs)),
  d => d.color
);

var output = rolled_up.map(([color, values]) => ({ key: color, values: values }));

console.log(output);
<script src="https://d3js.org/d3-array.v2.min.js"></script>

This:

  • Uses d3.rollups to group by color and reduce the resulting grouped values:
    • The 3rd parameter is the dimension on which to group items.
    • The 2nd parameter (vs => d3.transpose(vs.map(d => d.values)).map(vs => d3.sum(vs))) is the reducing function, transforming grouped values.
  • The reducing function makes use of d3.transpose to zip the grouped arrays, before summing each part of the resulting merged array
  • Maps elements produced by the rollup to the expected output format.

Here is the intermediate result produced by d3.rollups:

var input = [
  { type: "apple", color: "green", values: [1, 2, 3, 4] },
  { type: "apple", color: "red",   values: [5, 6, 7, 8] },
  { type: "grape", color: "green", values: [9, 10, 11, 12] },
  { type: "grape", color: "red",   values: [13, 14, 15, 16] }
];

var rolled_up = d3.rollups(
  input,
  vs => d3.transpose(vs.map(d => d.values)).map(vs => d3.sum(vs)),
  d => d.color
);

console.log(rolled_up);
<script src="https://d3js.org/d3-array.v2.min.js"></script>
like image 20
Xavier Guihot Avatar answered Oct 12 '22 23:10

Xavier Guihot