【问题标题】:How to group by multiple keys at the same time using D3?如何使用 D3 同时按多个键分组?
【发布时间】:2021-02-16 12:13:56
【问题描述】:

这可行,但我想知道是否有比使用ab 创建一个字符串然后拆分它更好的方法:

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

【问题讨论】:

  • 好问题!起初看起来很简单,结果却很棘手。我怀疑是否有更好的方法来使用像您的方法这样的组合键。你可以做一个嵌套汇总,但你必须循环遍历结果数组以将它们展平为所需的输出。
  • "Grouping objects in an array by multiple keys" 的边界副本。尽管考虑到答案的质量,我们最好关闭该问题作为该问题的副本。此外,使用“javascript group of objects by multiple keys”之类的方法访问 Google 会产生大量可能的方法,包括 D3 和 VanillaJS。
  • 此外,您可能会对我在 Robin 的回答中作为 comment 的一部分发布的性能结果感兴趣。

标签: javascript d3.js


【解决方案1】:

随着 d3 v7 的发布,现在有更好的方法来使用新的 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/d3-array@3.0.2/dist/d3-array.min.js"></script>

【讨论】:

    【解决方案2】:

    如您所知,d3.rollups() 将在您拥有多个键时创建嵌套数组:

    如果指定了多个键,则返回嵌套的 Map [或数组]。

    因此,由于d3.rollups 不符合您的需求,我相信创建一个普通的 JavaScript 函数会更容易(我知道您的标题中有 "using D3",但即使在一个 D3 代码没有什么能阻止我们编写 D3 没有的普通 JS 解决方案。

    在下面的示例中,我特意编写了一个详细的函数(使用 cmets),因此它的每个部分都很清晰,避免了可能使其非常短(但更神秘)的更复杂的功能。在这个函数中我使用reduce,所以数据数组只循环一次。 myKeys 是您将用于汇总的键数组。

    这是函数和 cmets:

    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;
      }, [])
    };
    

    这里是演示:

    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;
      }, [])
    };

    这是一个包含更复杂数据的演示:

    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;
      }, [])
    };

    最后,注意这个函数会推送重复的值(在上面的例子中d: [40, 40, 33])。如果这不是您想要的,那么只需检查重复项。

    【讨论】:

      【解决方案3】:

      下面的方法允许您删除split,但不会阻止为复合键创建字符串的需要。在这种情况下,使用JSON.stringify({a: d.a, b: d.b}) 而不是${d.a} ${d.b},允许map 返回一个对象,其中cd 属性可以分配给键的parse

      这保留了您的问题的一些“d3-ishness”以及rollups 的实用程序来处理为cd 创建数组。

      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}) // compare 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.com/ajax/libs/d3/6.5.0/d3.min.js"></script>

      该方法可以适应@Gerado Furtado 答案的可扩展性,但我担心它会变得有点忙:

      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 compound 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 compound 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.com/ajax/libs/d3/6.5.0/d3.min.js"></script>

      关于在rollups 中引入InternMap 和相关功能的讨论很有趣 - 但我看不出它已经准备好,或者它对你想做的事情有用。

      【讨论】:

      • 不幸的是,这种方法的性能比原来的方法更差,这也难怪字符串化/解析比字符串连接/拆分慢:jsbench.me/5akl9m19hq/1。虽然毫无疑问,我是 D3 的忠实粉丝,但我认为 Gerardo 的无 D3 解决方案是解决这个问题的首选方案。
      • InternMaps 从v6.4.0 开始被引入主包,并且已经被d3.rollups 使用。
      猜你喜欢
      • 1970-01-01
      • 2015-06-05
      • 1970-01-01
      • 2011-07-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多