D3, nested appends, and data flow

Javascriptd3.js

Javascript Problem Overview


I'm in the process of finally learning D3, and I stumbled upon a problem that I haven't been able to find an answer to. I'm not certain if my question is because I'm not thinking idiomatically with the library, or if it is because of a procedure that I am currently unaware of. I should also mention that I've only started doing web-related things in June, so I'm fairly new to javascript.

Say that we're building a tool that gives a user a list of foods with respective images. And lets add on the additional constraint that each list item needs to be labeled by a unique ID so that it can be linked to another view. My first intuition to solve this is to create a list of <div>'s each with their own ID, where each div has its own <p> and <img>. The resulting HTML would look something like:

<div id="chocolate">
  <p>Chocolate Cookie</p>
  <img src="chocolate.jpg" />
</div>
<div id="sugar">
  <p>Sugar Cookie</p>
  <img src="sugar.jpg" />
</div>

The data for this tool is in a JSON array, where an individual JSON looks like:

{ "label": "sugar", "text": "Sugar Cookie", "img": "sugar.jpg" }

Is there a way to do generate the HTML in one fell swoop? Starting with a base case of adding a div, the code might look something like:

d3.select(containerId).selectAll('div')                                                          
   .data(food)
   .enter().append('div')
   .attr('id', function(d) { return d.label; });

Now, what about adding a <div> with a <p> in it? My original thought was to do something like:

d3.select(containerId).selectAll('div')                                                          
   .data(food)
   .enter().append('div')
   .attr('id', function(d) { return d.label; })
       .append('p').text('somethingHere');

But, I see two problems with this: (1) how do you get the data from the div element, and (2) how can you append multiple children to the same parent in one declarative chain? I can't think of a way to make the third step where I would append on the img.

I found mention of nested selection on another post, which pointed to http://bost.ocks.org/mike/nest/. But is nested selection, and therefore breaking apart the appends into three chunks, appropriate/idiomatic for this situation? Or is there actually a well-constructed way to form this structure in one chain of declarations? It seems like there might be a way with subselections mentioned on https://github.com/mbostock/d3/wiki/Selections, but I'm not familiar enough with the language to test that hypothesis.

From a conceptual level, these three objects (div, p, and img) are treated more like one group rather than separate entities, and it would be nice if the code reflected that as well.

Javascript Solutions


Solution 1 - Javascript

You cannot add multiple child elements within one chained command. You will need to save the parent selection in a variable. This should do what you want:

var data = [{ "label": "chocolate", "text": "Chocolate Cookie", "img": "chocolate.jpg" },
        { "label": "sugar", "text": "Sugar Cookie", "img": "sugar.jpg" }];

var diventer = d3.select("body").selectAll("div")
    .data(data)
  .enter().append("div")
    .attr("id", function(d) { return d.label; });

diventer.append("p")
    .text(function(d) { return d.text; });

diventer.append("img")
    .attr("src", function(d) { return d.img; });​

See working fiddle: http://jsfiddle.net/UNjuP/

You were wondering how a child element like p or img, gets access to the data that is bound to its parent. The data is inherited automatically from the parent when you append a new element. This means that the p and img elements will have the same data bound to them as the parent div.

This data propagation is not unique for the append method. It happens with the following selection methods: append, insert, and select.

For example, for selection.append:

> # selection.append(name) > > Appends a new element with the specified name as the last child of > each element in the current selection. Returns a new selection > containing the appended elements. Each new element inherits the data > of the current elements, if any, in the same manner as select for > subselections. The name must be specified as a constant, though in the > future we might allow appending of existing elements or a function to > generate the name dynamically.

Feel free to ask about the details if something is not clear.


EDIT

You can add multiple child elements without storing the selection in a variable by using the selection.each method. You can then also directly access the data from the parent:

var data = [{ "label": "chocolate", "text": "Chocolate Cookie", "img": "chocolate.jpg" },
        { "label": "sugar", "text": "Sugar Cookie", "img": "sugar.jpg" }];

d3.select("body").selectAll("div")
    .data(data)
  .enter().append("div")
    .attr("id", function(d) { return d.label; })
    .each(function(d) {
        d3.select(this).append("p")
          .text(d.text);
        d3.select(this).append("img")
          .attr("src", d.img);
    });

Solution 2 - Javascript

Again, not substantively different, but my preferred method would be to use 'call'

var data = [{ "label": "chocolate", "text": "Chocolate Cookie", "img": "chocolate.jpg" },
        { "label": "sugar", "text": "Sugar Cookie", "img": "sugar.jpg" }];

d3.select("body").selectAll("div")
    .data(data)
  .enter().append("div")
    .attr("id", function(d) { return d.label; })
  .call(function(parent){
    parent.append('p').text(function(d){ return d.text; });
    parent.append('img').attr("src", function(d) { return d.img; });​
  });

you don't need to store any variables and you can factor out the called function if you want to use a similar structure elsewhere.

Solution 3 - Javascript

This isn't fundamentally different from nautat's answer, but I think that the code can be made a bit cleaner by saving the update selection rather than the enter selection, and getting the enter selection from it for the one operation that needs it (adding the surrounding div).

When you insert or append an element into the enter() selection, it's added to the update selection that you can work with afterward. This means that you can join the data, then add a div with the enter selection, and then when you append with the update selection, you'll be appending within the divs that you added in the enter selection:

var cookies = [
  { "label": "sugar", "text": "Sugar Cookie", "img": "sugar.jpg" },
  { "label": "chocolate", "text": "Chocolate Cookie", "img": "chocolate.jpg" }];

var cookie = d3.select("#cookie-jar").selectAll().data(cookies);
cookie.enter().append("div");
cookie.append("p").text(function(d){ return d.text });
cookie.append("img").attr("src",function(d){ return d.img });

#cookie-jar div { border: solid 1px black; }

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="cookie-jar"></div>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionConnor GramazioView Question on Stackoverflow
Solution 1 - JavascriptnautatView Answer on Stackoverflow
Solution 2 - JavascriptTom PView Answer on Stackoverflow
Solution 3 - JavascriptJoshua TaylorView Answer on Stackoverflow