2

I use the Zoomable Icicle layout example in D3 to visualize a folder hierarchy. I would like to hide certain folders based on whether the folder has been accessed before a certain date - which works using filter:

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

What I need to do is hide the children (sub-folders and files) of that parent folder that has been hidden OR show the children if the parent is shown.

How do I assign a filter value of a parent to its children?

Thanks!

sim1
  • 457
  • 1
  • 7
  • 26

4 Answers4

6

And one more for the hat trick...

Final option, that I thought of after all that, and I think it's the winner. Not only is it closer to the example you were working from, but it works for any of the D3 hierarchical layout functions. The secret: get D3 to do the work for you. Specifically:

  1. Use a D3 layout function to calculate a new layout that only includes the nodes that meet your filter criteria;
  2. For all the nodes that are part of the new layout, show them and update their position/size.
  3. Hide the nodes which don't have layout data in the new layout.

The trick is in step 1; getting the layout function to only include the nodes that meet your filter criteria. The .children() method of the partition layout function allows you to specify how the layout function identifies the children. The function in the example was:

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

Meaning it just expects the node to contain an array of child elements or else a number. If you only want some children included, all you have to do is go through the array of child elements and return the ones you want:

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;
    });

Of course, this assumes a simple data structure that is arrays of arrays of numbers. For your data structure you'll have to update both child-accessor functions.

The great thing about applying the filter in the child accessor function, is that once an element fails the filter, it's sub-children are automatically excluded too, since the layout function never even sees them. (No recursive functions required: D3 does it for you!)

To apply the new filtered layout, create an update function that takes as a parameter the layout function:

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

To apply the filter, call updateLayout(filteredPartition); to revert back to the unfiltered version, call updateLayout(partition) (where partition was the name of the original layout function from the example).

There's only a couple details left. First, to get it all started I needed to have the root data object that was used in the original layout. That means it needed to have been saved in a variable when the graph was first initialized. Second, we need a key function that can match up the new layout data objects with the old layout's data objects. Here's the necessary declarations and the initialization method updated to include them:

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
}

I don't know if that quite counts as a straightforward solution, but it's straightforward when compared to the other two answers.

Anyway, if you actually implement any or all of these approaches, I'd love to see the final product if you're able to post it online.

AmeliaBR
  • 27,344
  • 6
  • 86
  • 119
  • Hi Amelia, thanks so much for the great, in-detail answers. This third option seems to be the way to go and so I selected this one to be the answer. I will definitely try this one and if it works, I will let you know! If I have time, I will try the others too! Thanks a lot, much appreciate the effort that you put in to these answers, I understand everything a lot better now. – sim1 Dec 31 '13 at 15:44
  • Hi Amelia, I have had a go at this...I think this is what I need, but I am struggling to get it working 100%. I have tried to make a fiddle to show you what my code looks like: http://jsfiddle.net/Sim1/6BJdg/12/ but it's now not reading in the forEach loop (which is does in my code in Visual Studio). Could you please have a look? Firstly, it seems there is a problem with reading in the forEach, and secondly the first element is drawn and that's it. I'm not sure what is wrong. – sim1 Jan 27 '14 at 15:44
  • One problem is that I hadn't realize "formattedD" was a string, not a date -- you need to use actual date values in your comparison. Also, some of your folders had size values attached, and that meant that using the existence of a size property to test for leaf nodes was failing. However, **the bigger problem** is that the partition function is removing the "children" array before it can run the filter on it, and over-writing it later. You'll need to use a different property name for *all children* versus *filtered children* for layout. Updated fiddle: http://jsfiddle.net/6BJdg/14/ – AmeliaBR Jan 27 '14 at 18:38
  • Hi Amelia, apologies for only replying now. Thank you so much, I see your changes fixed everything. When I have a working solution of this answer I will post the link here. – sim1 Feb 04 '14 at 21:00
  • Hi Amelia, is there any chance that you can have a look at this question for me: http://stackoverflow.com/questions/21652037/how-to-have-scroll-like-behavior-to-navigate-between-folders-on-arrow-click-in-d I would really appreciate it! – sim1 Feb 09 '14 at 09:59
3

So, that was how to find children using only the relationships in the data objects. Why is it so much more complicated than my first answer? It's because I assumed (emphasis on the A-S-S!) that when you said "parent" and "child" you were talking about the hierarchy of the actual web page as well as of the data.

I haven't used the D3 hierarchical layout tools much, and I was surprised to discover that most of Mike's examples don't actually create a hierarchical DOM structure to match the hierarchical data structure. There is a justification in not doing so, in that you reduce the total number of elements in your webpage, but at the cost of losing the semantic structure.

By semantic structure, I mean one that is reflective of the actual meaning of the content. For example, if you have a file system like this:

C drive
  FolderA
    FileA1
    FileA2
  File1

Your DOM representing it would look like this:

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

By comparison, if you were to copy the approach from one of the layout examples(like this or this), you'd get something like this:

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

All the nodes are listed as siblings of each other, with nothing (not even a class) to differentiate between the root and the leaves. The Zoomable Icicle example is the same, except the nodes are just a single rectangle element rather than a group of rectangle plus text.

Now, the hierarchical DOM layout is a little bit more complex (and therefore has a slightly higher memory requirement for the browser). It also takes a little more code to create. But once you have it, then that's where my original comment of

If your child elements are actually DOM children of the parent element, then they will automatically inherit the display:none; or visibility:hidden; style settings from the parent element.

comes into play.

So the trick is just to create the DOM structure that matches the data structure. But I'm going to go one step further and suggest you use HTML DOM elements instead of SVG elements. The end result should look like this fiddle.

Why HTML elements? Because then the browser will automatically collapse your display, shifting over later elements to fill the space that is opened up when you hide other elements. Of course, you can't use HTML elements if you want to draw fancy shapes like the arcs used to create the "sunburst" pattern shown in the API. But you can always replace <div> elements with <g> elements to create a semantic SVG structure like I outlined above.

Now, one complication of the semantic approach is that we can't create all the nodes at once; we have to follow the data structure, making children as sub-elements of their parents. And since we don't know how many levels of children a given element will have, that means another recursive function. Recursive functions and tree data structures were made for each other. Literally. This time, instead of getting descendents based on the data, we'll be making the descendents based on the data:

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

To call this recursive function, we have to first use the partition layout function to analyze the data, but then we only attach the root data object directly to the top-level selection, and call the recursive function to create everything else. Instead of putting everything in an <svg> element, I'm going to put it all in an 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);
});

Finally (and this is important!), add the following style rules to your CSS:

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

This tells the <div> elements, which normally always start on a new row, to instead line themselves up nicely left-to-right, except for the nodes with class "first" (which we assigned to the nodes with index 0, i.e., the first child node of a given parent), which are told to start a new row. And the final rule is so that the height of the div will automatically include all the floating child elements.

And as to your original question on how to hide all the child elements of a folder, well now (after all that...) it is easy. Just hide the parent node, and all the child content will hide too:

figure.selectAll(".folder").filter(function (d) {
        return d.dateAccessed > formattedD; 
    })
    .style("display", "none");
AmeliaBR
  • 27,344
  • 6
  • 86
  • 119
2

Have you actually tried it? If your child elements are actually DOM children of the parent element, then they will automatically inherit the display:none; or visibility:hidden; style settings from the parent element.

If that doesn't work (for example, if you want the children to be hidden but not the parent, like a collapsed folder in a file menu, with the folder name visible but not its contents) then you just need to make a sub-selection after applying the filter to the main selection, like this:

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)
AmeliaBR
  • 27,344
  • 6
  • 86
  • 119
  • Hi Amelia, thanks for this answer. If you can possibly assist, using http://bl.ocks.org/mbostock/1005873 , can you help to create a selector for the '.children'? I haven't been able to figure that out yet - still new to D3 and anything I try doesn't work :\ – sim1 Dec 30 '13 at 20:55
  • In that example, as is, there isn't a selector that will work because the svg elements in the DOM structure are not arranged in a parent-child relationship to match the parent-child relationship in the data. Sorry, I should have looked up the example you were using before answering. I'm going to write out two other answers for options on how to proceed -- one for how to find the children using the elements as created in the example, and one for how to create an HTML DOM structure that matches your data structure so you can just use CSS to hide elements. – AmeliaBR Dec 30 '13 at 22:25
2

The example you linked to uses the D3.layout.partition functions to calculate the data objects for each node in the tree. That layout makes the following information available as properties of each data object (i.e., in a function(d){} context they can be accessed as d.propertyName, where the property names are as given in this list):

  • parent - the parent node, or null for the root.
  • children - the array of child nodes, or null for leaf nodes.
  • value - the node value, as returned by the value accessor.
  • depth - the depth of the node, starting at 0 for the root.
  • x - the minimum x-coordinate of the node position.
  • y - the minimum y-coordinate of the node position.
  • dx - the x-extent of the node position.
  • dy - the y-extent of the node position.

An important point to mention is that the parent and child properties are pointers (links) to the data objects of the corresponding nodes, not the DOM elements you later associate with the data. As a result, finding the data objects that go with your child elements of your selected parent elements is easy; finding the actual on-screen elements so we can hide them is harder.

Using only the information we have in the data object, finding the actual DOM object (in order to be able to hide it) would require selecting all the rectangles in the DOM, and then checking each one's data object to see if it matched one of our child data objects. This could end up being very slow. A better solution is, when you create the rectangles, modify the data object so that it includes a link back to the rectangle. Like this:

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
});

The .datum(d) method sets individual data objects, one at a time, for all the elements within a D3 selection. By using a function(d){} call within the method, and then returning d at the end of the function, we are taking the existing data object (assigned in the .data() method call two lines earlier), modifying it, and then re-assigning it back to the element.

How do we modify it? We create a new property, d.DOMobject and set it to the this keyword. In the context of the function call, this will reference the javascript object for the specific <rect> element that has this data object attached to it.

Now, anytime we can access the data object -- even if it is through a different node's parent or child links -- we can connect back up to the correct DOM element.

So now back to your filter. What we need is to create a new D3 selection that consists of the filtered elements and all their children, so that we can then apply styles to all at once. To do this, we're going to need a recursive function. A recursive function is one that calls itself, so that you can repeat it as many times as needed. We're going to use it to find children, and then find the children's children, and so on.

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

The forEach() method is a Javascript method on arrays that works very similar to the D3 each() method, except that the d values passed to the anonymous function are the actual entries in the array -- which in this case are all data objects created by the D3.layout.partition function. The arrayA.concat(arrayB) method returns the two arrays joined end-to-end.

To get the recursive function started, we're going to call it using the each() method on your filtered selection. However, we'll have to do one more layer of concatenation to connect up the results from each of your filtered folders.

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

The filtered variable is now an array of DOM elements consisting of the folders that passed the filter, plus all their descendents; thankfully, the d3.selectAll function can turn an array of DOM elements into a D3 selection. I'm not sure whether your filter is written so as to select the elements you want to hide or to select the ones you want to show, but if you want to show the elements that pass the filter than the round-about way to do that is to hide everything and then show the ones in our filter selection:

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

All the above works if all you want to do is hide the rectangles, by making them transparent. If you also wanted to collapse in the rest of the chart, shifting rectangles over to fill in the gap, it gets a lot more complicated, requiring a lot of calculations. Wait for my other answer for a better way to approach the problem if you need to do this.

P.S. Haven't actually tested these code snippets, I hope I've explained it clearly enough that you can figure out where an error is coming from if I've made any typos.

AmeliaBR
  • 27,344
  • 6
  • 86
  • 119