How to group by multiple keys at the same time using D3?

This works, but I was wondering if there was a better way than creating a string with a and b and later splitting it:

const data = [
    { a: 10, b: 20, c: 30, d: 40 },
    { a: 10, b: 20, c: 31, d: 41 },
    { a: 12, b: 22, c: 32, d: 42 }
];

d3.rollups(
    data,
    x => ({
      c: x.map(d => d.c),
      d: x.map(d => d.d)
    }),
    d => `${d.a} ${d.b}`
  )
  .map(([key, values]) => {
    const [a, b] = key.split(' ');
    return {a, b, ...values};
  });

// OUTPUT
// [
//   {a: "10", b: "20", c: [30, 31], d: [40, 41]},
//   {a: "12", b: "22", c: [32], d: [42]}
// ]

4 thoughts on “How to group by multiple keys at the same time using D3?”

  1. If you look at the source code of d3.rollups() you’ll see that it will create nested arrays if you have more than one key, and that’s expected.

    Therefore, I’d simply create a plain JavaScript function for your case (I’m aware of "using D3" in your title, but even in a D3 code nothing forbids us of writing plain JS solutions where D3 has none). In this example, I’m purposefully writing a verbose function (with comments) so each part of it is clear, avoiding more complex features (like destructuring assignement, for instance) which could make it substantially short. In this function I’m using reduce, so the data array is looped only once. myKeys is the array of keys you’ll use to rollup.

    Here is the function and the comments:

    function groupedRollup(myArray, myKeys) {
      return myArray.reduce((a, c) => {
        //Find the object in the acc with all 'myKeys' equivalent to the current
        const foundObject = a.find(e => myKeys.every(f => e[f] === c[f]));
        //if found, push the value for each key which is not in 'myKeys'
        if (foundObject) {
          for (let key in foundObject) {
            if (!keys.includes(key)) foundObject[key].push(c[key]);
          };
        //if not found, push the current object with all non 'myKeys' keys as arrays
        } else {
          const copiedObject = Object.assign({}, c);//avoids mutation
          for (let key in copiedObject) {
            if (!keys.includes(key)) copiedObject[key] = [copiedObject[key]];
          };
          a.push(copiedObject);
        };
        return a;
      }, [])
    };
    

    Here is the demo:

    const data = [{
        a: 10,
        b: 20,
        c: 30,
        d: 40
      },
      {
        a: 10,
        b: 20,
        c: 31,
        d: 41
      },
      {
        a: 12,
        b: 22,
        c: 32,
        d: 42
      }
    ];
    const keys = ["a", "b"];
    
    console.log(groupedRollup(data, keys))
    
    function groupedRollup(myArray, myKeys) {
      return myArray.reduce((a, c) => {
        const foundObject = a.find(e => myKeys.every(f => e[f] === c[f]));
        if (foundObject) {
          for (let key in foundObject) {
            if (!keys.includes(key)) foundObject[key].push(c[key]);
          };
        } else {
          const copiedObject = Object.assign({}, c);
          for (let key in copiedObject) {
            if (!keys.includes(key)) copiedObject[key] = [copiedObject[key]];
          };
          a.push(copiedObject);
        };
        return a;
      }, [])
    };

    And here is a demo with a more complex data:

    const data = [{
        a: 10,
        b: 20,
        c: 30,
        d: 40,
        e: 5,
        f: 19
      },
      {
        a: 10,
        b: 55,
        c: 37,
        d: 40,
        e: 5,
        f: 19
      },
      {
        a: 10,
        b: 20,
        c: 31,
        d: 48,
        e: 5,
        f: 18
      },
      {
        a: 80,
        b: 20,
        c: 31,
        d: 48,
        e: 5,
        f: 18
      },
      {
        a: 1,
        b: 2,
        c: 3,
        d: 8,
        e: 5,
        f: 9
      },
      {
        a: 10,
        b: 88,
        c: 44,
        d: 33,
        e: 5,
        f: 19
      }
    ];
    const keys = ["a", "e", "f"];
    
    console.log(groupedRollup(data, keys))
    
    function groupedRollup(myArray, myKeys) {
      return myArray.reduce((a, c) => {
        const foundObject = a.find(e => myKeys.every(f => e[f] === c[f]));
        if (foundObject) {
          for (let key in foundObject) {
            if (!keys.includes(key)) foundObject[key].push(c[key]);
          };
        } else {
          const copiedObject = Object.assign({}, c);
          for (let key in copiedObject) {
            if (!keys.includes(key)) copiedObject[key] = [copiedObject[key]];
          };
          a.push(copiedObject);
        };
        return a;
      }, [])
    };

    Finally, pay attention that this function will push duplicated values (in the above example d: [40, 40, 33]). If that’s not what you want then just check for duplicates.

    Reply

Leave a Comment