javascript - How to group by multiple keys at the same time using D3? - Stack Overflow

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

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]}
// ]

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]}
// ]
Share Improve this question edited Feb 17, 2021 at 0:14 Gerardo Furtado 102k9 gold badges128 silver badges177 bronze badges asked Feb 16, 2021 at 12:13 nachocabnachocab 14.5k21 gold badges104 silver badges158 bronze badges 3
  • 2 Good question! Looking simple at first, it turns out to be quite tricky. I doubt there is a better way to use a bined key like your approach. You could do a nested rollup but then you'd have to loop through the resulting arrays to flatten them to the desired output. – altocumulus Commented Feb 16, 2021 at 16:04
  • Borderline duplicate of "Grouping objects in an array by multiple keys". Although we might be better off closing that one as a duplicate of this question given the quality of answers. Also, hitting Google with something like "javascript group array of objects by multiple keys" yields a plethora of possible approaches, both D3 as well as VanillaJS. – altocumulus Commented Feb 17, 2021 at 16:23
  • Additionally, you might be interested in the performance results I posted as part of my ment on Robin's answer. – altocumulus Commented Feb 17, 2021 at 16:25
Add a ment  | 

3 Answers 3

Reset to default 4

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/npm/[email protected]/dist/d3-array.min.js"></script>

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 ments) so each part of it is clear, avoiding more plex 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 ments:

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 plex 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.

The approach below allows you to remove the split, but does not prevent the need to create a string for the pound key. In this case, using JSON.stringify({a: d.a, b: d.b}) instead of ${d.a} ${d.b}, allows for the map to return an object where the c and d properties can be assigned to the parse of the key.

This preserves some of the 'd3-ishness' of your question and the utility of rollups to deal with the creation of the arrays for c and d.

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 groups = d3.rollups(
  data,
  x => ({
    c: x.map(d => d.c),
    d: x.map(d => d.d)
  }),
  d => JSON.stringify({a: d.a, b: d.b}) // pare with `${d.a} ${d.b}`
).map(arr => Object.assign(JSON.parse(arr[0]), arr[1]));

console.log(groups);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare./ajax/libs/d3/6.5.0/d3.min.js"></script>

The approach can acmodate the extensibility of @Gerado Furtado's answer, but I fear it's getting a little hectic:

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"];

const groupedRollup = (data, keys) => {
  const others = Object.keys(data[0])
    .filter(k => !keys.includes(k)); // finds b, c, d as not part of pound key
    
  return d3.rollups(
    data,
    x => Object.assign(
      {}, 
      ...others.map(k => {
        return {[k]: x.map(d => d[k])} // dynamically create reducer
      })
    ),
    d => JSON.stringify(
      Object.assign(
        {}, 
        ...keys.map(k => {
          return {[k]: d[k]} // dynamically add keys
        })
      )
    ) // and stringify for pound key
  ).map(arr => Object.fromEntries( // sorting the output object
    Object.entries( // keys in alpha order
      Object.assign(JSON.parse(arr[0]), arr[1])).sort() // same approach
    )
  );
}

console.log(groupedRollup(data, keys));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare./ajax/libs/d3/6.5.0/d3.min.js"></script>

There's some interesting talk about the introduction of use of InternMap in rollups and the associated functions - but I don't see either that it's ready, or that it's useful for what you are trying to do.

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1742416557a4439856.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信