Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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]}
// ]
like image 341
nachocab Avatar asked Feb 16 '21 12:02

nachocab


People also ask

What is rollup in d3?

rollup is a powerful new function for grouping and summarising data in d3-array@2. However, it doesn't always return the data structure you're after. Depending on whether you opt for rollup or rollups you'll get a Map or an array : d3.rollup d3.rollups. Map(2) {"🍑" => 5, "🍐" => 2}

What is d3 nest?

nest() function is used to group the data as groupBy clause does in SQL. It groups the data on the basis of keys and values. Syntax: d3.nest() Parameters: This function does not accept any parameters. Return Value: It returns the object with several properties as entries, keys, values, map, sort.

What is d3 extent?

extent() function in D3. js is used to returns the minimum and maximum value in an array from the given array using natural order. If an array is empty then it returns undefined, undefined as output.


2 Answers

With d3 v7 released, there is now a better way to do this using the new d3.flatRollup.

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 result = d3.flatRollup(
    data,
    x => ({
      c: x.map(d => d.c),
      d: x.map(d => d.d)
    }),
    d => d.a,
    d => d.b
  );
console.log(result);

const flattened = result.map(([a, b, values]) => ({a, b, ...values}));
console.log(flattened);
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/d3-array.min.js"></script>
like image 74
waternova Avatar answered Oct 17 '22 09:10

waternova


As you already know d3.rollups() will create nested arrays if you have more than one key:

If more than one key is specified, a nested Map [or array] is returned.

Therefore, as d3.rollups doesn't fit your needs, I believe it's easier to create a plain JavaScript function (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 the following example I'm purposefully writing a verbose function (with comments) so each part of it is clear, avoiding more complex features which could make it substantially short (but more cryptic). 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.

like image 2
Gerardo Furtado Avatar answered Oct 17 '22 07:10

Gerardo Furtado