复杂的d3.nest()操作

时间:2014-01-03 22:15:42

标签: d3.js

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

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 data format

我想用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在评论中的答案很好,但在我的情况下,我的树太深了,无法重复将.keys()应用于数据,所以我无法手动编写这样的函数。

4 个答案:

答案 0 :(得分:12)

这是一个更简单的函数,只使用嵌套的for - 循环来循环遍历每组数组中的所有路径指令。

为了更容易找到具有给定标签的子元素,我已将children实现为数据对象/关联数组而不是编号数组。如果你想要非常健壮,可以使用d3.map来解释该链接所描述的原因,但是如果你的标签实际上是整数而不是问题。无论哪种方式,它只是意味着当你需要作为一个数组访问子节点时(例如,对于d3布局函数),你必须指定一个函数来从对象的值中创建一个数组 - {{3实用程序功能为你做到了。

关键代码:

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

使用示例数据数组和基本的d3.values(object)图表方法实现:
cluster dendogram

编辑添加: 正如评论中所提到的,要使输出完全符合要求:

  1. 从默认根对象的children数组中访问数据的根对象。
  2. 使用递归函数循环遍历树,用子数组替换子对象。
  3. 像这样:

    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 :(得分:1)

如果扩展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来完成的。一开始,prevsnull,不会过滤任何内容。
  • 对于这些数组,我们得到与树中当前级别(i元素)对应的值。重复过滤。这是由.map().getUnique()
  • 完成的
  • 对于我们这样获得的每个值,都会有一个返回值。所以我们迭代它们(vals.map())。对于每个,我们设置label属性。其余代码确定是否有子节点并通过递归调用获取它们。为此,我们首先检查数组中是否还有元素,即如果我们处于树的最深层。如果是这样,我们进行递归调用,传入包含我们当前正在处理的元素和下一级(prev)的新i+1。最后,我们检查这个递归调用的结果是否为空元素 - 如果只有空子元素,我们不保存它们。这是必要的,因为并非所有阵列(即并非所有路径)都具有相同的长度。

答案 2 :(得分:0)

由于不赞成使用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));
<script src="https://d3js.org/d3-array.v2.min.js"></script>

此:

  • 作为对数组深度的递归。
  • 每个递归步骤将其数组分组(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]));
<script src="https://d3js.org/d3-array.v2.min.js"></script>

答案 3 :(得分:0)

编辑-固定

这是我的解决方案 Pro:一劳永逸(不需要将对象转换为上述数组) 优点:保持大小/值计数 Pro:输出与带有孩子的D3耀斑完全相同 缺点:比较丑陋,效率可能较低 非常感谢以前的评论对我的帮助。

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