d3 Treemap with Title Headers

The D3 library is really a great way to create data visualizations in HTML5. I absolutely love it and its treemap is especially powerful. I wanted to create a treemap that showed grouping headers like the JIT version that has been around for a while, but it took a little more work to get D3 to do the same thing. I came across a post that showed the attempts of others to create the same thing and I wanted to show the code of how I did it in case others are interested. I created two versions: the first uses SVG labels which are nice because you can measure them to decide what room is available for displaying a label in a given cell. However, wrapping is much more involved and ended up creating a second version which uses embedded HTML divs to create self wrapping labels which seem to look a little better. I also modified the examples of others in the thread so instead of showing labels where there is enough room to see them, I instead took the approach of showing labels for given group when that group was selected. (NOTE: The labels do not work in IE in the first and third demos because IE is a horrible browser. I could write checks to use svg elements instead of foreignobjects for the labels but you get what you need from the second demo if you want IE compatibility.)     So here is the first example which uses divs for the labels: (link to code) (bl.ocks.org)
 

  …and here is the second version which uses SVG text elements: (link to code) (bl.ocks.org)

 
UPDATE: Here is the third version which uses IDs instead of the name so duplicates titles will work (notice the two ‘display’ groups on the left side): (link to code)

 
UPDATE #2: In the comments for this post I received a request to show how one might highlight the children on mouseover. I’ve modified one of the above examples to demonstrate this:


Basically, I just added code to modify the child blocks on mouseover like this:

    var childEnterTransition = childrenCells.enter()
        .append("g")
        .attr("class", "cell child")
        .on("click", function(d) {
            zoom(node === d.parent ? root : d.parent);
        })
        .on("mouseover", function() {
            this.parentNode.appendChild(this); // workaround for bringing elements to the front (ie z-index)
            d3.select(this)
                .attr("filter", "url(#outerDropShadow)")
                .select(".background")
                .style("stroke", "#000000");
        })
        .on("mouseout", function() {
            d3.select(this)
                .attr("filter", "")
                .select(".background")
                .style("stroke", "#FFFFFF");
        });
I added an SVG filter (in the section) and just modified the background border stroke of each child on mouseover. I also called appendChild on the parent container to make sure the moused-over child had the highest z-index within the groups of child nodes, otherwise the filtering gets clipped by any child added after the current child. Here is a Gist showing the code.

UPDATE: #3: I received a question about how to space out the parent/child groups so I put this demo together to show one possible avenue to implement this. I essentially increased the treemap layout padding and then used the d.dy value to size the parent background to occupy the container’s full height rather than just the header height. (It is not necessary for this demo but you might need to sort your nodes to make sure the parent nodes are added before the child nodes and in the correct order to avoid having larger squares covering smaller ones.). To see the changes you can pull the source code for all of these treemap demos here and then do a diff between treemap_header_01.html and treemap_header_05.html to see the changes made to implement this. (the gist shows the treemap_header_02.html demo first so scroll down to find the source for the others)


89 comments on “d3 Treemap with Title Headers

  1. Hi Bill,

    Thanks for making these modifications, they’re really helpful. I have a relatively small data set and would like to bring back the text of the child nodes in the top-level view of the treemap. Do you have any tips on where to get started? I’ve been looking through your code but its a bit daunting for me since I’m new to d3.js

    Cheers

    • No problem. The code hides the labels for any cell/node whose parent is not equal to the ‘current’ level. Put another way, if you have a treemap with 4 groups, each with 4 child cells, the initial level is 0 and which means all 4 groups are parented by the node at level 0, so show their labels. When you click on one of those groups, the selected group becomes the new ‘current’ level. So when the zoomTransition completes, it re-evaluates all the cells and only the cells whose parent is the ‘current’ level get their labels displayed. So if you want to show all levels all the time, just remove/comment-out that display changing logic. I think it is primarily done at the start and end of the transitions, so start by just removing the lines where you see a ‘display’ style being set and that should stop the logic which basically hides all labels on the start of a transition and then, we the transition is done, re-displays the labels whose parent is the newly selected node.

  2. Looks like your override for naming the cells actually breaks the tree. If the names are duplicated only one will render.

    • No, that’s not really true. You just have to avoid using the name property in the identity function if you want to avoid issues with duplicate titles. I used names in the identity function for the first two examples for simplicity. If you are not sure how to use the identity function, I just posted a third version showing how to use IDs instead of names which allows duplicate titles with no problems.

  3. Thank you Bill; this is very nice! However, I had trouble when I ran your versions with d3.v3 instead of v2. For the third version above, and linking to the original flare.json data, I get just one little blue block using version 3.

    • @Donna,

      The third version relies upon the data having and ID property that is used in the parentCells and childrenCells selections. The flare.json data file contains no unique IDs so that explains why it doesn’t work. The reason the third one uses IDs (unlike the first two examples) is because using unique IDs allows us to display nodes with the same name.

      πŸ™‚
      Bill

    • Hi there, just wondering if you got anywhere with the switch to v3 from v3, before I try to figure it out….

      Thanks!

      Chris

    • Not sure what you mean: are you wanting to link to something outside the treemap? If so, I’m sure you could add a mouse listener on the domNode that routes the user to a given URL. Or are you asking if the link would drill-in even further? The existing code does that already.

      • The drilling definitely works great. I’m wondering then what’s the easiest approach to make the labels in the lowest level items into anchors that link to something (<a href…) or attach a mouselistener so I can run some javascript when the deepest element is selected.

        • I figured it out…I changed the last child DIV to be an A and then added an href attribute so that now when I drill all the way down, the final element is a link to a more in-depth view.

          • Good deal. I was just about to reply and suggestion something along those lines. If you wanted to get fancy, you could also try adding a click handler on the svg group for that cell so the entire square becomes a button which takes the user to a url. So when creating those cells do something like this:


            childCellSelection.append("g")
            .on("click", function(d) {
            // code here to open link URL
            });

          • Flex,
            I am so glad to find this post. I am implementing the similar function using d3 treemap with final elements as URL links. Since I am very new to d3, would you please share more in detail about how you changed the last child DIV to be an A? Thanks a lot.

  4. Hi.. I tried giving names inside the child divs and removed the part where the opacity is set to 0 or 1 based on the text width and box width.. Ive added the code
    childEnterTransition.append(‘text’)
    .attr(“class”, “label”)
    .attr(‘x’, function (d) {
    return d.dx / 2;
    })
    .attr(‘y’, function (d) {
    return d.dy / 2;
    }).attr(“dy”, “.35em”)
    .attr(“overflow”, “hidden”)
    .attr(“text-overflow”, “ellipsis”)
    .attr(“text-anchor”, “middle”)
    .style(“display”, “block”)
    .style(“font-size”, “9px”)
    .text(function (d) {
    return d.name;
    })
    but the text is shown as full and it gets overlapped and its a mess when we see it. Do you have any clue, what could be wrong??

    and text-anchor : start wont also work

  5. Yes.. Im trying to display all the names all the time… but Im trying to trim the text to fit the box if text width is greater than box width..

  6. Hi Bill,
    Thankyou for this great layout.
    I am having trouble with the headers overlapping with the cells.
    As you can see with Flex (the smallest and the first parent in your example above). I was wondering how to fix this problem πŸ™‚ I have been playing with the layout and it seems i dont have quite a good understanding of d3 to work it out myself.

    • I see what you are referring to. I went back and reviewed these demos (which I took from other sources and modified) and found a lot of things that I either missed or did wrong. I’ve updated the examples so that most of the issues should now be resolved. The one exception being IE which I simply did not have time to address (only the 2nd demo will behave in IE) but all the demos will now at least start in all browsers (which support SVG).

  7. Hi Bill

    I would like to congratulate you on this treemap – it is just about what I was looking for.

    I would like to know if there is any way to allow the children rectangles to highlight on hover? If so, can you let me know what to include and where to include it?

    Many thanks!

    Regards

  8. Ok, forgive me for asking another question. I was wondering if you can advise on this as well:

    I would like to still on-click zoom in to the children, but once the zoom is completed be able to select another child within that same parent block without it zooming out to the original display.

    Possibly then only zooming out on the root heading (could we make the heading static somehow?).

    Is this possible?

    Many thanks.

    • If you were to change line 158 of the Gist I posted to be this:

      var parentEnterTransition = parentCells.enter()
                  .append("g")
                  .attr("class", "cell parent")
                  .on("click", function(d) {
                      if (node !== d) {
                          zoom(d);
                      } else {
                          zoom(d.parent);
                      }
                  });
      

      and then change line 211 to be this:

              var childEnterTransition = childrenCells.enter()
                  .append("g")
                  .attr("class", "cell child")
                  .on("click", function(d) {
                      zoom(d.parent);
                  })
      

      then it should work like you want.

      • Hi Bill

        I followed your suggestion and it works as required. πŸ™‚ πŸ™‚

        Many, many thanks for your assistance and well done again on your great work.

        Regards
        Simone

      • Hi Bill,

        I really like the above steps. Its works fine..
        On-click I need the last child node name now i am getting the immediate parent name by adding the console.log(d.name) in the zoom function
        Please help on this

  9. Hi Bill

    I am currently modifying the D3 Zoomable Pack Layout example found here: http://mbostock.github.io/d3/talk/20111116/pack-hierarchy.html

    to work similarly to the treemap that you’ve posted.

    I show all labels (including children) initially – the only problem that I am left with is that for small circles that are close together the labels overlap, and so I would like to truncate them with the ellipsis as you have done in your treemap example.

    I have read your post on truncating labels, but am not sure where to modify the code on the link to include the handling of the overlapping text.

    Is there any chance that you can help me with this?

    Many thanks for all your assistance.

    Regards
    Simone

  10. Pingback: Bill White's Blog | Circle Pack Label Truncation

  11. Hi Bill,

    This is a great example. But the one thing I can’t seem to figure out is how to add more text inside the cell (example #2) when you zoom in. I’m thinking the best approach would be to append tspan elements at this point zoomTransition.select(".child .label") and then just select the tspan elements and remove them when zooming back out. No matter what I try

    var childText = zoomTransition.select(".child .label")
    .attr("x", function (d) {
    return kx * d.dx / 2;
    })
    .attr("y", function (d) {
    return ky * d.dy / 2;
    })
    .text("");

    childText.each(function (d) {
    this.append('tspan')
    .attr('dy', 10)
    .text("Me");
    });

    I can’t seem to attach tspan elements to this svg:text element. Do you have thoughts?
    Thanks,
    Chris

  12. Hi Bill,

    Like many others, I have benefited from these great treemaps. I just have a small comment and I am not sure how obvious this is…but when preparing my own data for the 3rd graph and having to consider adding some 300+ IDs, I realized I could just simply do this:

    var parentCells = chart.selectAll("g.cell.parent")
    .data(parents, function(d,i) {
    //return "p-" + d.id; // nf: original from Bill
    return "p-" + (++i);
    });

    And then apply the same strategy for the children. This way there is no need for explicit IDs in the data itself.

    • That works in this particular situation, but you have to be careful when using an incrementing counter to assign IDs in the d3 identity function. Taking a step back from this example, you need to know that, if you do not provide an identity function (the function that is passed as the second argument in the data() call), then d3 uses the data’s index as the ID (see the data() function documentation for more details on that). That ID is how d3 knows what DOM node is tied to what data element. When you subsequently update that data element, d3 can then find and update its corresponding DOM node. As long as your data is always in the same order/position, things work fine. However, if the order of your data changes, things will start to go wrong since the ID of your data will now be out of sync with the corresponding DOM node. Going back to this particular treemap example, we only load the data once and never update it, so you do not hit this problem. Just keep in mind that if you ever work with data that changes during the life of the visualization, relying upon an incrementing counter to hand out ID values will come back to haunt you. πŸ™‚

  13. Hi Bill,

    You Rock! thanks for this!

    Question: If I wanted to populate the final children with image, video, and other styled content, what would be the best way to do that? I’m having trouble and have tried a couple different SVG drawing methods, some other reference functions, but best I can do is link to other content off-site.

    Any idea(s) on how to accomplish this with D3 and JSON…?

    Again, thanks for all your work, you are awesome.

    Best,

    CK

    • You can use the “ForeignObject” tag to create normal HTML5 content inside the SVG elements but you will probably run into some problems with IE. That’s about the only way I know how to do something like that unless you wanted to use d3 to compute the layout positions of the items and then pair it with a canvas library (like EaselJS) to render the resulting treemap on a Canvas. At that point, I would think you could render other type of content within the treemap, but I’ve never tried it that way.

  14. Hiya. I love this example and I’m adapting it for use inside a larger modular system. One new thing I’m trying to do though is to keep the title-headers of the higher levels visible when the user zooms in.

    Basically the model is that the user can move around in the treemap and when they select a leaf node, we zoom into that leaf’s parent, that leaf remains selected and the click drives functionality elsewhere on the page. Now after their little drilldown story is done though I need a way for the user to get back to a higher zoomlevel in order to potentially drilldown on a different leaf.

    The obvious design I think is for the higher level titles to sort of stack up at the top of the layout. In other words for the titles to not really be part of the treelayout anymore when we start zooming in…

    You can get some partial success by mucking with the xscale and yscale bounds, ie changing
    this.yscale.domain([d.x, d.y + d.dy]);
    to
    this.yscale.domain([0, d.y + d.dy]);

    but this is a terrible hack and has some horrible side effects on the layout. I mention it only cause it’s a quick and dirty way to see what I mean.

    • I took the obvious approach of just creating a little stack of identically styled and clickable divs just above the svg canvas, to hold the headers for the layers that you lose when you zoom in. I’m sure it can be done more elegantly in d3 and during the transitions it looks a little jarring, but the end result is good enough for my purposes. Thanks again.

      • I’ve actually been working with something similar lately. My current task involves requesting additional data when the user clicks deeper into the treemap. I achieved the effect you are referring to by simplying creating a breadcrumb bar at the top of the treemap and when the user clicks on a cell or sub-group, I drill down to that sub-group (either the one that was selected or the parent of the cell that was selected) and simultaneously add a breadcrumb over the title of the subgroup which gets transitioned up into the breadcrumb bar. Clicking the breadcrumb bar reverses the drill in and returns the user to the previous level. My design uses the normal d3 treemap layout, but you might also look at Bostock’s version of this. He actually only uses the treemap layout to get the correct hierarchy and some initial positioning and then he calculates the layout manually on drill in to keep the ideal aspect ratio for the current depth rather than letting the treemap optimize the aspect ratio for all depths including those that he is not showing (he only shows two levels at a time).

        • Hi Bill,

          A great article, and a very similar functionality I was working at.
          You were referring to the Bostock’s version of this for two levels, and I wanted each level to be lazily loaded as given in that article.
          Can you please guide me on how can we do the lazy loading, and update the current treemap without altering its structure.

          I would be making another call to the server to get the data of the next level. Once, I retreive the data, I need to add to the child element and show its treemap.
          Can you please help me with this solution.

          Thanks,
          Arvind

          • This is a much tougher problem than the article let’s on. The demo assumes that if you are going to show two levels of depth in the Treemap, that you can safely retrieve that amount at all times. But what if you need to be able to handle a larger amount of data? The solution for me has been to make the server that returns the data use a limit (say 500) and it will return that number of items and anything beyond that get’s categorized into an “other” group.

            For example, if I had 2 top level groups each with 300 children and the server’s limit is 500, the server would return me the 2 top level groups, each with 248 items plus an “other” group with a size value representing the remaining data. Why 248 you ask? Well, remember that the server limit is 500, which results in 2 * (1 top level group, 1 other group, 248 items).

            Now, this is where things get interesting: since my server limit is 500, if I drill-in to one of the two groups, that same 500 limit is re-applied to the subgroup query, leaving more than enough capacity to show all 300 items in that single subgroup once I’ve zoomed into that level. However, the demo in the article lays out the children before hand so the only way to make the transition between levels work smoothly is to handle the treemap layout math/logic so the children that were already drawn before the click stay where they are and the “other” group is replaced by the 52 entering children (remember that at the top level, each group had 248, but now on drill-in, there are 300 in the zoomed group meaning we have 52 items being added to the 248 already draw at the previous level). Because the demo assumes you have the ability to draw all children available at each layer shown, it does not readily meet my needs for a lazily-loaded tree.

            Now, with that being said, if you are not worried about that type of limit, you can simply modify the display() method so that, instead of the click listener just calling transition(), it would instead call a method that retrieves more data (probably child date of the group that was clicked). When that data is returned, you could append that new data to the child that was clicked and then proceed with the transition() call which will now have more data to process and layout as the deeper level.

            If you get it working, I’d love to see how it comes out. πŸ™‚

  15. Pingback: Part III, Topic and Lyrical Content Correlation : Deciphering Chaos

  16. Hi Bill,

    Thanks for sharing. This is very useful. I want to do small aesthetic modifications to this but can’t seem to figure out how to go about it.

    What I want to do is separate the bigger parent cell from others with a larger margin like shown here in Mike Bostock’s version http://bost.ocks.org/mike/treemap/. And also have the parent header inside that bigger cell, instead of a separate title cell in your version.

    I hope I made myself clear. Can you help me out with this?

    • You should be able to configure the treemap padding parameter to get the effect you want. Take a look at this page in the docs and see if that gets you going in the right direction.

      • I’ll have a look at this. Appreciate your reply.

        Also, I tried to get the padding is increase the stroke-width for the parent cell like

        .parent rect {
        stroke-width: 4px;
        }

        This seems to work only for the “title header” cell not “entire cell-group” of the parent. I think if there’s a way to change the scope of the parent to the parent-cell level, it might be possible. Any pointers on doing that Bill?

        • I added a 5th demo to the blog post (see the last part titled Update #3) showing one way you might add spacing around the groups. Not sure if it is exactly what you want but it should get you started. There is a link to the example code there as well.

  17. Great tutorial!

    How can we get our hands on the data files used to produce these tree maps though?
    I can see references to ../data/flare1.json from the source code but could not find the data files in the git repo.

    Thanks,
    Michel

  18. Great help … thank you… It would be great if you can help me with the tooltip for child cells….
    Tank you in advance

  19. Pingback: Zoomable Treemap | Thao's Data Science Projects

  20. Bill:
    Thank you for the wonderful demo and all the dedicated support.
    I found a way to work around the duplicate name problem without requiring the unique ID fields in the JSON (so it will still work on the original flare.json file.
    The idea is to use the full hierarchical name for each child.
    This can be done as follows:

    var children = nodes.filter(function(d) {
    if (d.parent) {
    d.name = d.parent.name + "/" + d.name;
    }
    return !d.children;
    });

    This results in each child having a unique name, and therefore you first version of the code works perfectly. To shorten the display name for each child you can now use:

    .text(function(d) {
    var names = d.name.split('/');
    return names[names.length-1];
    });

    this displays only the last part of the fully qualified name, A side benefit is that the fully qualified name is available for use, for example in a hover-tip.

  21. Bill:
    Thank you for the wonderful d3 demo and the dedicated support.
    I think I found a way around the duplicate name problem without adding a unique ID field to the json. The idea is to use the full hierarchical name for each child. This will make the names unique. This can be done as follows:

    var children = nodes.filter(function(d) {
    if (d.parent) {
    d.name = d.parent.name + "/" + d.name;
    }
    return !d.children;
    });

    If you find the names taking up too much display space then add the following wherever you reference the text

    .text(function(d) {
    var names = d.name.split('/');
    return names[names.length-1];
    });

    The above will now properly render the original flare.json file. It has the side benefit that the fully qualified name if available if you want to use it, for example in a hover-over tip.

  22. We are trying to use d3 library and facing couple of issues with it.

    issue 1 ) when we use group by, it will start showing blank/white spaces when tree is not balanced evenly
    issue 2) when we set zooming to true, we stop seeing the text and see the blank boxes.

    Can you please help with these issues?

    • Sorry, but I don’t have enough information here to really help. You should try posting a demo of the problem and then ask your question on the d3 forum here. That way lots of d3 developers will be able to see the issue and provide assistance.

  23. Hi Bill,
    thanks for your demo, it’s really helpful. I’m trying to do a treemap quite similar. The only difference is that my hierarchy is deeper (7 levels max) so putting headers on all the subcategories would take too much space on the global size of my treemap and i loose visibiliy on the leaves.

    What i’m trying to do is to only show the titles of highest categories in the hierarchy and show the titles of the next level when i zoom in one of these categories. Do you have any advice that could help me to progress?

    (Hope my english is not too bad and it was clear ^^” )

    Thanks

    • The only way I can think of to get that to work is to implement your own custom treemap layout. You’d need to override the behavior to return a 0 padding for all depths below a given level. Just a guess but it seems like that might work.

  24. Pingback: Big Data Taxonomy Visualisation | Artek Consulting Company Blog

  25. Thanks a lot for publishing your work on this. This has helped us build a very functional TreeMap to display the Big Data landscape.

    Not only does it help visualising complex taxonomies with several hierarchical levels, but it also is a joy to use in order to navigate the nomenclature and discover details as you go.
    As you can see in our example linked above, we have customised it a little more, adding textual details and links to the leaf items.

    Many thanks

  26. Hi, there
    I have used zoomable treemap before; but is there a way to use zoomable treemap with dynamic children. In others word I will go to the server for the next level through an API.

    • I wonder if you could get the effect you are seeking by rendering a new “sub-treemap” within a given square from a parent treemap? You would only load the child treemap when the user zooms in beyond a given level, at which point you’d issue a new query and recursively load a child treemap with the overall bounds matching the dimensions of the parent treemap’s corresponding child square. It might not be too easy, but theoretically it would work…

  27. Hi Bill,

    I am writing to report a minor bug with your code. Consider this json:

    var data =
    {
    “name”: “root”,
    “children”: [
    { ‘name’: ‘root.child1’, ‘children’: [ { ‘name’ : “root.child1.child1”, ‘size’ : 1 } ] },
    { ‘name’: ‘root.child2’, ‘children’: [ { ‘name’ : “root.child2.child1”, ‘size’ : 1 } ] }
    ]
    };

    It works fine. Now consider a hierarchy where children names names collide.

    var data =
    {
    “name”: “root”,
    “children”: [
    { ‘name’: ‘root.child1’, ‘children’: [ { ‘name’ : “name-collison”, ‘size’ : 1 } ] },
    { ‘name’: ‘root.child2’, ‘children’: [ { ‘name’ : “name-collison”, ‘size’ : 1 } ] }
    ]
    };

    When the treemap operates upon this, the 2nd instance of the sub-category is left blank.

    In a hierarchy, it should be perfectly valid to have things like:

    java.io
    jboss.io

    however, in this implementation, the “io” collision will cause problems.

    Hope that makes sense.

    Thanks,

    – Pat

    • Yes: I suppose you could use the parent’s id (assuming there is an id in the dataset as a name is not unique enough) in the id function for the child nodes to give them contextual association which would avoid the collisions between duplicate child nodes residing amongst multiple parent groups.

  28. Hi Bill,
    Is there a way that we can highlight the whole parent cell on hovering the mouse over a child element. Like if i hover my mouse on a child element of physics the whole physics border gets highlighted. (example-5).

    Thanks.

  29. How to set colors according to size of cells? Like boxes above certain size be green less then some size should be red. Can you please guide me.

Leave a Reply

Your email address will not be published. Required fields are marked *