【问题标题】:Complex d3.nest() manipulation复杂的 d3.nest() 操作
【发布时间】:2014-01-03 22:15:41
【问题描述】:

我有一个如下所示的数组:

var arrays = [[1,2,3,4,5],
              [1,2,6,4,5],
              [1,3,6,4,5],
              [1,2,3,6,5],
              [1,7,5],
              [1,7,3,5]]

我想使用d3.nest() 甚至只是标准的javascript 来将这些数据转换成一个嵌套的数据结构,我可以使用d3.partition

具体来说,我想创建这种flare.json 数据格式。

我想用d3.nest() 创建的json 对象的级别对应于数组中的索引位置。请注意,1 在上面示例数据中的所有子数组中位于首位;因此,它位于树的根部。在数组的下一个位置有三个值,237,因此,根值 1 有 3 个子值。此时树看起来像这样:

      1
    / | \
   2  3  7

在子数组的第三个位置有四个值,356。这些孩子将按如下方式放入树中:

            1
        ____|___
       /    |    \
      2     3     7
     / \   /     / \
    3   6 6     3   5

如何使用d3.nest() 生成此数据结构?我上面展示的示例数据的完整数据结构应该如下所示:

   {"label": 1, 
     "children": [
        {"label": 2, "children": [
            {"label": 3, "children": [
                {"label": 4, "children": [
                    {"label": 5}
                ]},
                {"label": 6, "children": [
                    {"label": 5}
                ]}
            ]},
            {"label": 6, "children": [
                {"label": 4, "children": [
                    {"label": 5}
                ]}
            ]},
        {"label": 3, "children": [
            {"label": 6, "children": [
                {"label": 4, "children": [
                    {"label": 5}
                ]}
            ]}
        ]},
        {"label": 7, "children": [
            {"label": 3, "children": [
                {"label": 5}
            ]},
            {"label": 5}
        ]}
      ]}
    ]}

我正在尝试使用类似这样的方法转换上面的数组数据结构(非常错误):

var data = d3.nest()
  .key(function(d, i) { return d.i; })
  .rollup(function(d) { return d.length; })

我已经用了一周的时间来尝试了解如何从数组数组中生成这种分层数据结构。如果有人可以帮助我,我将不胜感激。

@meetamit 在 cmets 中的回答很好,但在我的情况下,我的树太深,无法重复将 .keys() 应用于数据,所以我无法手动编写这样的函数。

【问题讨论】:

  • 首先,由于您想要多个嵌套级别,因此需要多次调用d3.nest().key(...)。第一次调用key(function() {}) 时传入的函数将产生第一级;第二个函数将产生第二个级别,等等。查看this fiddle 中的控制台输出。它应该可以帮助您弄清楚如何将其转换为您想要的形式。
  • 这非常有帮助。谢谢。
  • 听起来你想要一个递归函数而不是d3.nest
  • 我一直在找at this function,和我要找的差不多;但是,我似乎无法正确调整它以产生正确的树结构。

标签: d3.js


【解决方案1】:

这是一个更直接的函数,它只使用嵌套的for-loops 循环遍历每个数组集中的所有路径指令。

为了更容易找到具有给定标签的子元素,我将children 实现为数据对象/关联数组,而不是编号数组。如果你想变得非常健壮,你可以使用d3.map,原因在该链接中描述,但如果你的标签实际上是整数,那么这不会成为问题。无论哪种方式,它只是意味着当您需要以数组的形式访问子项时(例如,对于 d3 布局函数),您必须指定一个函数来从对象的值中创建一个数组 - d3.values(object)实用函数为您完成。

关键代码:

var root={}, 
    path, node, next, i,j, N, M;

for (i = 0, N=arrays.length; i<N; i++){
    //for each path in the data array 
    path = arrays[i];
    node = root; //start the path from the root

    for (j=0,M=path.length; j<M; j++){
        //follow the path through the tree
        //creating new nodes as necessary

        if (!node.children){ 
            //undefined, so create it:
            node.children = {}; 
        //children is defined as an object 
        //(not array) to allow named keys
        }

        next = node.children[path[j]];
        //find the child node whose key matches
        //the label of this step in the path

        if (!next) {
            //undefined, so create
            next = node.children[path[j]] = 
                {label:path[j]};
        }
        node = next; 
        // step down the tree before analyzing the
        // next step in the path.        
    }    
}

使用您的示例数据数组和基本的cluster dendogram 图表方法实现:
http://fiddle.jshell.net/KWc73/

编辑添加: 如 cmets 中所述,要使输出看起来完全符合要求:

  1. 从默认根对象的子数组访问数据的根对象。
  2. 使用递归函数循环遍历树,将子对象替换为子数组。

像这样:

root = d3.values(root.children)[0];
//this is the root from the original data, 
//assuming all paths start from one root, like in the example data

//recurse through the tree, turning the child
//objects into arrays
function childrenToArray(n){
    if (n.children) {
        //this node has children

        n.children = d3.values(n.children);
        //convert to array

        n.children.forEach(childrenToArray);
        //recurse down tree
    }
}
childrenToArray(root);

更新小提琴:
http://fiddle.jshell.net/KWc73/1/

【讨论】:

  • P.S.该代码创建了一个未标记的根节点,因为它使代码更简单,并且允许数据中的所有路径可能不以相同的标签开头。如果您不想在图表中包含那个额外的根,只需使用 root = d3.values(root.children)[0] 跳到默认根的第一个(并且仅在这种情况下)子级。
  • 图表+1!看起来我们的两种方法是对偶的——你一个一个地做,我一个级地做。
  • 谢谢。这很酷,虽然不完全正确。您的解决方案在顶层没有“标签”,孩子应该是一个列表,您将它们作为字典。
  • @turtle:除了告诉你如何修复它们之外,我通常不会对这些细节大惊小怪。但既然你在这个问题上得到了很大的赏金,我想我最好按照要求明确回答。已编辑。
  • @LarsKotthoff:没错。同一个问题的不同方法。我的直觉是,对于大型数据集,这种方法会比你的方法稍微快一些,因为我没有制作临时数组或使用过滤器,但最后的额外递归步骤可能会抵消一些优势。我声称的另一个优点是代码更易于阅读/理解——但话说回来,我有偏见,因为我编写了代码!
【解决方案2】:

如果你扩展Array 的规范,它实际上并没有那么复杂。基本思想是逐级构建树,一次获取每个数组元素并与前一个元素进行比较。这是代码(减去扩展名):

function process(prevs, i) {
  var vals = arrays.filter(function(d) { return prevs === null || d.slice(0, i).compare(prevs); })
                 .map(function(d) { return d[i]; }).getUnique();
  return vals.map(function(d) {
    var ret = { label: d }
    if(i < arrays.map(function(d) { return d.length; }).max() - 1) {
        tmp = process(prevs === null ? [d] : prevs.concat([d]), i+1);
        if(tmp.filter(function(d) { return d.label != undefined; }).length > 0)
          ret.children = tmp;
    }
    return ret;
  });
}

不能保证它不会因边缘情况而中断,但它似乎可以很好地处理您的数据。

完整的 jsfiddle here.

一些更详细的解释:

  • 首先,我们获取与当前路径相关的数组。这是通过filter 剔除与prevs 不同的那些来完成的,prevs 是我们当前的(部分)路径。一开始,prevsnull,没有过滤任何内容。
  • 对于这些数组,我们得到对应于树中当前级别的值(ith 元素)。重复项被过滤。这是由.map().getUnique() 完成的。
  • 对于我们通过这种方式获得的每个值,都会有一个返回值。所以我们遍历它们 (vals.map())。对于每个,我们设置label 属性。其余代码确定是否有孩子并通过递归调用获取它们。为此,我们首先检查数组中是否还有元素,即我们是否处于树的最深层次。如果是这样,我们进行递归调用,传入新的prev,其中包括我们当前正在处理的元素和下一个级别(i+1)。最后,我们检查这个对空元素的递归调用的结果——如果只有空的子元素,我们不保存它们。这是必要的,因为并非所有数组(即并非所有路径)都具有相同的长度。

【讨论】:

  • 哇,太棒了!我不擅长 JS,所以我需要一些时间来理解你的代码。我可以看到的一件事是终端节点看起来有些不正确。它们有空数组和对象。例如,[1,7,3,5] 具有 {name: 5, children: []}[1,7,5] 具有 {name: 5, children: [[{children:[]}]}
  • 我注意到了这一点,并在我所做的最新编辑中修复了它(至少我是这么认为的——如果我错了,请告诉我)。
  • 我不能说我喜欢在初始化例程中弄乱数组原型的想法,但这个概念是可靠的。这绝对不是d3.nest 的情况。
  • 你并没有真正搞乱Array 原型——扩展标准功能是一件很常见的事情,一点也不难。
  • @LarsKotthoff 也许我仍然对 JavaScript 的所有可能性感到不满意;如果我要实现你的算法,我会创建一个继承自 Array 的专用类。但我想如果页面上的另一个脚本也扩展 Array 具有相同的函数名称但不同的功能,这只会是一个问题。这是可能的,但不太可能。
【解决方案3】:

由于d3-collection 已被d3.array 弃用,我们可以使用d3.groups 来实现以前使用d3.nest 的功能:

var input = [
  [1, 2, 3, 4, 5],
  [1, 2, 6, 4, 5],
  [1, 3, 6, 4, 5],
  [1, 2, 3, 6, 5],
  [1, 7, 5],
  [1, 7, 3, 5]
];

function process(arrays, depth) {
  return d3.groups(arrays, d => d[depth]).map(x => {
    if (
      x[1].length > 1 ||                     // if there is more than 1 child
      (x[1].length == 1 && x[1][0][depth+1]) // if there is 1 child and the future depth is inferior to the child's length
    )
      return ({
        "label": x[0],
        "children": process(x[1], depth+1)
      });
    return ({ "label": x[0] });              // if there is no child
  });
};

console.log(process(input, 0));
&lt;script src="https://d3js.org/d3-array.v2.min.js"&gt;&lt;/script&gt;

这个:

  • 作为数组深度的递归。
  • 每个递归步骤将其数组分组 (d3.groups) 在其索引等于深度的数组元素上。
  • 根据是否有子节点,递归停止。

这是d3.groups 在递归步骤中产生的中间结果(在第三个元素上对数组进行分组):

var input = [
  [1, 2, 3, 4, 5],
  [1, 2, 6, 4, 5],
  [1, 2, 3, 6, 5]
];

console.log(d3.groups(input, d => d[2]));
&lt;script src="https://d3js.org/d3-array.v2.min.js"&gt;&lt;/script&gt;

【讨论】:

    【解决方案4】:

    编辑 - 已修复

    这是我的解决方案 优点:一气呵成(不需要像上面那样将对象转换为数组) 优点:它保持大小/值计数 亲:输出与带孩子的 d3 耀斑完全相同 缺点:它更丑陋,而且效率可能更低 非常感谢以前的 cmets 帮助我解决了这个问题。

        var data = [[1,2,3,4,5],
            [1,2,6,4,5],
            [1,3,6,4,5],
            [1,2,3,6,5],
            [1,7,5],
            [1,7,3,5]]
    
        var root = {"name":"flare", "children":[]} // the output
        var node // pointer thingy
        var row
    
    
    
    // loop through array
    for(var i=0;i<data.length;i++){
    
    row = data[i];
    node = root;
    
        // loop through each field
        for(var j=0;j<row.length;j++){
    
            // set undefined to "null"
            if (typeof row[j] !== 'undefined' && row[j] !== null) {
                attribute = row[j]
            }else{
                attribute = "null"
            }
    
            // using underscore.js, does this field exist
            if(_.where(node.children, {name:attribute}) == false  ){
    
    
                if(j < row.length -1){
                    // this is not the deepest field, so create a child with children
                    var oobj = {"name":attribute, "children":[] }
                    node.children.push(oobj)
                    node = node.children[node.children.length-1]
                }else{
                    // this is the deepest we go, so set a starting size/value of 1
                    node.children.push({"name":attribute, "size":1 })
                }
    
            }else{
    
                // the fields exists, but we need to find where
                found = false
                pos = 0
    
                for(var k=0;k< node.children.length ;k++){
                    if(node.children[k]['name'] == attribute){
                        pos = k
                        found = true
                        break       
                    }
                }
    
                if(!node.children[pos]['children']){      
                    // if no key called children then we are at the deepest layer, increment
                    node.children[pos]['size'] = parseInt(node.children[pos]['size']) + 1
                }else{
                    // we are not at the deepest, so move the pointer "node" and allow code to continue
                    node = node.children[pos]
                }
            }
        }
    }
    
    
    
    // object here
    console.log(root)
    
    // stringified version to page
    document.getElementById('output').innerHTML = JSON.stringify(root, null, 1);
    

    工作示例 https://jsfiddle.net/7qaz062u/

    输出

    { "name": "flare", "children": [ { "name": 1, "children": [ { "name": 2, "children": [ { "name": 3, "children": [ { "name": 4, "children": [ { "name": 5, "size": 1 } ] } ] }, { "name": 6, "children": [ { "name": 4, "children": [ { "name": 5, "size": 1 } ] } ] } ] }, { "name": 3, "children": [ { "name": 6, "children": [ { "name": 4, "children": [ { "name": 5, "size": 1 } ] } ] }, { "name": 3, "children": [ { "name": 6, "children": [ { "name": 5, "size": 1 } ] } ] } ] }, { "name": 7, "children": [ { "name": 5, "size": 1 }, { "name": 3, "children": [ { "name": 5, "size": 1 } ] } ] } ] } ] }
    

    【讨论】:

      猜你喜欢
      • 2022-01-13
      • 2016-01-04
      • 2016-11-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多