Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

d3.js : create an expanding list viz using selection.on

Tags:

d3.js

I have a data structure like

[
    {"name":"foo", "links":["a", "b", "c"]}, 
    {"name":"bar", "links":["d", "e", "f"]}
]

I'd like to make a list like

<ul>
    <li> foo </li>
    <li> bar </li>
</ul>

which, when I click on foo, expands like

<ul>
    <li> foo 
        <ul>
            <li> a </li>
            <li> b </li>
            <li> c </li>
        </ul>
    </li>
    <li> bar </li>
</ul>

I'd like to do this using d3.js. It's easy and fun to make the top list:

toplist = d3.select("body").append("ul");
toplist.selectAll("li")
    .data(data)
    .enter()
    .append("li")
        .text(function(d){return d.name;})

It's even easy to add the sublists:

items = toplist.append("ul")
    .selectAll("li")
    .data(function(d){return d.links})
    .enter()
    .append("li")
         .text(function(d){return d})

I can add an on("click".. to each li in toplist, like

toplist.selectAll("li")
    .data(data)
    .enter()
    .append("li")
        .text(function(d){return d.name;})
        .on("click", expand)

and move the sublist code into a function called expand. It looks like

function expand(d,i){

and then has the above items = ... code in it.

The trouble is that this then expands both foo and bar at the same time.

How do I give the foo list element a separate on click event to bar which expands only that element? Essentially, I need to pass expand the index of the data element, and have it only operate on that specific piece of the DOM.

Any help would be appreciated!

like image 485
Mike Dewar Avatar asked Feb 01 '26 22:02

Mike Dewar


1 Answers

You have everything you need when expand is called. The this context is the li element that was clicked, and d is the associated data. If you only want to expand (rather than toggle between expand and collapse), then you can create the sublist by selecting this:

function expand(d) {
  d3.select(this)
      .on("click", null)
    .append("ul")
    .selectAll("li")
      .data(d.links)
    .enter().append("li")
      .text(function(d) { return d; });
}

Note that expand also removes the click handler from the clicked item; you don't want to add the list again when clicking twice.

If you want to toggle between expand and remove, then you need to track some state: whether the submenu is open or closed. You could either do this by storing a boolean on d (d.open, say), or you could do it by querying the DOM to see if the submenu already exists: d3.select(this).select("ul").empty().

Yet another way you could do this would be to create all the submenus ahead of time, and then just toggle the visibility via the display style property. That's the simplest option if you have all of the data available; I'd probably only bother will creating the submenu dynamically if you were loading the link data asynchronously.

Live example: http://bl.ocks.org/1541816

like image 90
mbostock Avatar answered Feb 04 '26 15:02

mbostock