如何根据父级是否在D3中过滤来过滤子级?

时间:2013-12-26 17:38:27

标签: filter d3.js

我使用D3中的Zoomable Icicle布局示例来可视化文件夹层次结构。我想根据文件夹是否在特定日期之前被访问来隐藏某些文件夹 - 使用过滤器:

.filter(function (d) {
    return d.dateAccessed > formattedD; //formattedD is the date two weeks ago
})

我需要做的是隐藏已隐藏的父文件夹的子文件夹(子文件夹和文件);如果显示父文件,则显示子文件。

如何为其子项分配父级的过滤值?

谢谢!

4 个答案:

答案 0 :(得分:5)

还有一个hat trick ...

最终选项,我想到之后所有这一切,我认为这是胜利者。它不仅更接近您所使用的示例,而且适用于任何D3分层布局功能。秘诀:让D3为你做的工作。具体做法是:

  1. 使用D3布局功能计算仅包含符合过滤条件的节点的新布局;
  2. 对于属于新布局的所有节点,显示它们并更新其位置/大小。
  3. 隐藏新布局中没有布局数据的节点。
  4. 诀窍在第1步;使布局功能仅包含符合筛选条件的节点。分区布局功能的.children() method允许您指定布局功能如何识别子项。示例中的函数是:

    var partition = d3.layout.partition()
        .children(function(d) { return isNaN(d.value) ? d3.entries(d.value) : null; })
        .value(function(d) { return d.value; });
    

    意思是它只是希望节点包含一个子元素数组或一个数字。如果你只想要一些孩子,你要做的就是浏览子元素数组并返回你想要的子元素:

    var filteredPartition = d3.layout.partition()
        .value(function(d) { return d.value; })
        .children(function(d){
    
           if isNaN(d.value) {
             var filteredChildren = [];
             d3.entries(d.value).forEach(function(d2){
               if (d2.dateAccessed < formattedD) filteredChildren.push(d);
               //a.push(d) adds d to the end of the array 'a'
             });
             return filteredChildren;
           //Note that the nodes which PASS the filter test will be shown
           //and the nodes that fail will be hidden; make sure your filter
           //is written in the way you want.
           }
           else return null;
        });
    

    当然,这假设一个简单的数据结构,即数组数组。对于您的数据结构,您必须更新两个子访问器功能。

    在子访问器函数中应用过滤器的好处是,一旦元素失败了过滤器,它的子子项也会被自动排除,因为布局函数甚至从未看到它们。 (不需要递归函数:D3为你做!)

    要应用新的过滤布局,请创建一个更新函数,该函数将布局函数作为参数:

    var updateLayout(layoutFunction){
    
        var newRects = rects.data(layoutFunction(rootData), keyFunction)
                .transition().duration(500)
                .style("visibility", "visible")
                .attr("x", function(d) { return x(d.x); })
                .attr("y", function(d) { return y(d.y); })
                .attr("width", function(d) { return x(d.dx); })
                .attr("height", function(d) { return y(d.dy); });
    
        newRects.exit()
                .transition().duration(500)
                .style("visibility", "hidden");
           //don't delete, just hide;  these will still be part 
           //of the rects selection for the next update.
    }
    

    要应用过滤器,请致电updateLayout(filteredPartition);要恢复到未过滤的版本,请调用updateLayout(partition)(其中partition是示例中原始布局函数的名称)。

    只剩下几个细节。首先,为了让它全部启动,我需要拥有原始布局中使用的根数据对象。这意味着在首次初始化图形时需要将其保存在变量中。其次,我们需要一个能够将新布局数据对象与旧布局的数据对象相匹配的关键函数。这是必要的声明和更新初始化方法以包含它们:

    var keyFunction = function(d) {
    
        return d.FileName; 
        //or something else from your data structure that uniquely 
        //identifies each file
    
        //You could even do it recursively to get the full, unique file path:
        //return (d.parent == null) ? 
        //    "d.FileName" : keyFunction(d.parent) + "/" + d.FileName;
    }
    var rootData;
    
    d3.json("readme.json", function(error, root) {
      rootData = d3.entries(root)[0];
      rect = rect.data(partition(rootData), keyFunction)
         .enter()
      //...and the rest is the same
    }
    

    我不知道这是否算得上是一个直截了当的解决方案,但与其他两个答案相比,它是直截了当的。

    无论如何,如果您实际实施任何或所有这些方法,我很乐意看到最终产品,如果您能够在线发布。

答案 1 :(得分:2)

因此,这就是如何仅使用数据对象中的关系来查找子项。为什么它比我的第一个答案复杂得多?这是因为我假设(强调 A-S-S!),当你说“父母”和“孩子”时,你谈论的是实际网页的层次结构以及数据。

我没有太多地使用D3分层布局工具,我很惊讶地发现Mike的大多数示例实际上并没有创建一个分层DOM结构来匹配分层数据结构。有一个理由不这样做,因为你减少了网页中元素的总数,但是以丢失语义结构为代价。

通过语义结构,我的意思是反映内容的实际含义。例如,如果您有这样的文件系统:

C drive
  FolderA
    FileA1
    FileA2
  File1

表示它的DOM看起来像这样:

<g class="node depth0">
  <rect ...>
  <text>C drive</text>
  <g class="children">
    <g class="node depth1">
       <rect ...>
       <text>FolderA</text>
       <g class="children">
         <g class="node depth2">
            <rect ...>
            <text>FileA1</text>
         </g>
         <g class="node depth2">
            <rect ...>
            <text>FileA2</text>
         </g>
       </g>
    <g class="node depth1">
       <rect ...>
       <text>File1</text>
    </g>
  </g>
</g>

相比之下,如果您要从其中一个布局示例(like thisthis)复制该方法,您将得到以下内容:

<g class="node">
  <rect ...>
  <text>C drive</text>
</g>
<g class="node">
  <rect ...>
  <text>FolderA</text>
</g>
<g class="node">
   <rect ...>
   <text>FileA1</text>
</g>
<g class="node">
   <rect ...>
   <text>FileA2</text>
</g>
<g class="node">
   <rect ...>
   <text>File1</text>
</g>

所有节点都被列为彼此的兄弟节点,没有任何东西(甚至不是类)来区分根和叶子。 Zoomable Icicle示例是相同的,除了节点只是一个矩形元素而不是一组矩形加文本。

现在,分层DOM布局有点复杂(因此对浏览器的内存要求略高)。它还需要更多的代码来创建。但是一旦你拥有它,那就是我原来的评论

  

如果您的子元素实际上是父元素的DOM子元素,   然后他们会自动继承display:none;visibility:hidden;   来自父元素的样式设置。

发挥作用。

所以诀窍就是创建与数据结构匹配的DOM结构。但我将更进一步,建议您使用HTML DOM元素而不是SVG元素。最终结果应该看起来像this fiddle

为何选择HTML元素?因为浏览器会自动折叠您的显示,移动后面的元素以填充隐藏其他元素时打开的空间。当然,如果要绘制花哨的形状(如用于创建“sunburst”模式shown in the API的弧形),则不能使用HTML元素。但是,您始终可以使用<div>元素替换<g>元素,以创建如上所述的语义SVG结构。

现在,语义方法的一个复杂因素是我们无法一次创建所有节点;我们必须遵循数据结构,将孩子作为父母的子元素。由于我们不知道给定元素将具有多少级别的子级,这意味着另一个递归函数。递归函数和树数据结构相互制作。从字面上看。这一次,我们不是根据数据得到后代,而是根据数据制作后代:

var makeDescendents = function(d,i){
   //This function takes the <div> element that called it and appends
   //a "rectangle" label for this data object, and then creates a
   //a hierarchy of child <div> elements for each of its children

   var root = d3.select(this)
           .attr("class", function(d){return "depth" + d.depth;})
           .classed("node", true)
           .classed("first", function(d,i){return i==0;})
           .style("width", function(d){return d.dx;});

   //add a p to act as the label rectangle for this file/folder
   //and style it according to the passed-in d object
   root.append("p")  
       //you could also use a <div> (or any other block-display element)
       //but I wanted to keep the nodes distinct from the labels
       .classed("node-label", true)
       .style("height", function(d) {return d.dy})
       .style("background-color", function(d) { 
                  return color((d.children ? d : d.parent).key); 
            })
       .on("click", clicked)
       //And so on for any other attributes and styles for the rectangles
       //Remembering to use HTML/CSS style properties (e.g. background-color)
       //not SVG style attributes (e.g. fill) 
       .text(function(d) {return d.name;}); 
          //(or however the label value is stored in your data)

    if (d.children === null){
       //This object doesn't have any children, so label it a file and we're done.
       root.classed("file", true);
       return;
    }
    else {
       //Label this a folder, then
       //create a sub-selection of <div> elements representing the children
       //and then call this method on each of them to fill in the content
       root.classed("folder", true)
           .selectAll("div.node")
           .data(function(d) {return d.children;})
         .enter()
           .append("div")
           .call(makeDescendents);
       return;
    } 
} 

要调用此递归函数,我们必须首先使用分区布局函数来分析数据,但是我们只将根数据对象直接附加到顶级选择,并调用递归函数来创建其他所有内容。不是将所有内容放在<svg>元素中,而是将其全部放在HTML <figure> element中:

var figure = d3.select("body").append("figure")
    .attr("width", width)
    .attr("height", height);

var rootNode = figure.selectAll("figure > div.node");

d3.json("readme.json", function(error, rootData) {
  rootNode = rootNode
      .data(partition(d3.entries(rootData)[0])[0])
    .enter().append("div")
      .call(makeDescendents);
});

最后(这很重要!),将以下样式规则添加到CSS中:

div.node {
  float:left;
}
div.node.first {
  clear:left;
}
div.node::after, figure::after {
  content:"";
  display:block;
  clear:both;
}

这告诉<div>元素,通常总是从一个新行开始,而不是从左到右排列自己,除了类“first”的节点(我们分配给节点)索引0,即给定父节点的第一个子节点,被告知开始一个新行。最后的规则是div的高度为automatically include all the floating child elements

关于如何隐藏文件夹中所有子元素的原始问题,现在(毕竟......)之后很容易。只需隐藏父节点,所有子内容也将隐藏:

figure.selectAll(".folder").filter(function (d) {
        return d.dateAccessed > formattedD; 
    })
    .style("display", "none");

答案 2 :(得分:1)

你真的尝试过吗?如果您的子元素实际上是父元素的DOM子元素,那么它们将自动从父元素继承display:none;visibility:hidden;样式设置。

如果这不起作用(例如,如果您希望隐藏子项而不是父项,如文件菜单中的折叠文件夹,文件夹名称可见但不是其内容)那么您只需要将过滤器应用于主选择后进行子选择,如下所示:

folders.filter(function (d) {
        return d.dateAccessed > formattedD; 
        //formattedD is the date two weeks ago
    })
    .style("opacity", "0.5") 
        //example only, would create a greyed out folder name
    .selectAll(".children") 
            //or whatever selector you had used to identify the child elements
        .style("display", "none");
            //hide and collapse (visibility:hidden would hide but not collapse)

答案 3 :(得分:1)

您链接的示例使用D3.layout.partition函数计算树中每个节点的数据对象。该布局使以下信息可用作每个数据对象的属性(即,在function(d){}上下文中,它们可以作为d.propertyName访问,其中属性名称如此列表中所示):

  • parent - 父节点,或root的null。
  • children - 子节点数组,或叶节点为null。
  • value - 值访问器返回的节点值。
  • depth - 节点的深度,从0开始。
  • x - 节点位置的最小x坐标。
  • y - 节点位置的最小y坐标。
  • dx - 节点位置的x范围。
  • dy - 节点位置的y范围。

值得一提的是,parentchild属性是指向相应节点的数据对象的指针(链接),而不是稍后与数据关联的DOM元素。因此,查找与所选父元素的子元素一起使用的数据对象很容易;找到实际的屏幕元素,这样我们就可以隐藏它们了。

仅使用数据对象中的信息,找到实际的DOM对象(为了能够隐藏它),需要在DOM中选择 all 矩形,然后检查每个一个人的数据对象,看它是否与我们的一个子数据对象匹配。这可能最终会非常缓慢。更好的解决方案是,在创建矩形时,修改数据对象,使其包含返回矩形的链接。像这样:

d3.json("readme.json", function(error, root) {
  rect = rect
      .data(partition(d3.entries(root)[0]))
    .enter().append("rect")
      .datum(function(d) { d.DOMobject = this; return d; })
      .attr("x", function(d) { return x(d.x); })
      //...etc. for the rest of the initialization code
});

.datum(d)方法为D3选择中的所有元素设置单个数据对象,一次一个。通过在方法中使用function(d){}调用,然后在函数末尾返回d,我们将获取现有数据对象(在.data()方法调用中分配前两行) ,修改它,然后将其重新分配回元素。

我们如何修改它?我们创建了一个新属性d.DOMobject,并将其设置为this关键字。在函数调用的上下文中,this将引用附加了此数据对象的特定<rect>元素的javascript对象。

现在,只要我们可以访问数据对象 - 即使它是通过不同节点的父链接或子链接 - 我们就可以连接回正确的DOM元素。

现在回到你的过滤器。我们需要的是创建一个由过滤后的元素及其所有子元素组成的新D3选择,以便我们可以同时将样式应用于所有元素。为此,我们需要一个递归函数。递归函数是一个自我调用函数,因此您可以根据需要多次重复它。我们将用它来寻找孩子,然后找到孩子的孩子,等等。

var getDescendents = function(d) {
    //This function will return an array of DOM elements representing
    //the element associated with the passed-in data object and all it's
    //child elements and their children and descendents as well.

    if (d.children === null){
       //This object doesn't have any children, so return an array of length 1
       //containing only the DOM element associated with it.
       return [d.DOMobject];
    }
    else {
       //Create an array that consists of the DOM element associated 
       //with this object, concatenated (joined end-to-end) 
       //with the results of running this function on each of its children.
       var desc = [d.DOMobject]; //start with this element
       d.children.forEach(function(d){desc = desc.concat(getDescendents(d) );});
       return desc;
    } 
} 

forEach()方法是数组上的Javascript方法,与D3 each()方法非常相似,只是传递给匿名函数的d值是数组中的实际条目 - 在这种情况下,是由D3.layout.partition函数创建的所有数据对象。 arrayA.concat(arrayB)方法返回端到端连接的两个数组。

要启动递归函数,我们将使用过滤选择的each()方法调用它。但是,我们必须再进行一层连接,以连接每个已过滤文件夹的结果。

var filtered = [];
folders.filter(function (d) {
        return d.dateAccessed > formattedD; 
    })
    .each(function(d){filtered = filtered.concat(getDescendents(d) );});

filtered变量现在是一个DOM元素数组,由传递过滤器的文件夹及其所有后代组成;幸运的是,d3.selectAll函数可以将DOM元素数组转换为D3选择。我不确定您的过滤器是否已编写,以便选择要隐藏的元素或选择要显示的元素,但是如果要显示通过过滤器的元素而不是圆形方式那就是隐藏所有内容然后在我们的过滤器选择中显示:

rect.style("visibility", "hidden");
  //assumes rect is a variable referencing all nodes in the tree, 
  //as in the example

d3.selectAll(filtered).style("visibility", "visible");

如果您想要做的只是隐藏矩形,通过使它们透明,以上所有工作。如果您还想在图表的其余部分折叠,将矩形移动以填补间隙,则会变得更加复杂,需要进行大量计算。如果您需要这样做,请等待我的其他答案,以便更好地解决问题。

P.S。实际上没有测试过这些代码片段,我希望我已经清楚地解释了它,如果我犯了任何错别字,你可以弄清楚错误的来源。