Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3, nested appends, and data flow

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.

like image 821
Connor Gramazio Avatar asked Nov 02 '12 22:11

Connor Gramazio


2 Answers

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);
    });
like image 69
nautat Avatar answered Oct 27 '22 19:10

nautat


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.

like image 39
Tom P Avatar answered Oct 27 '22 21:10

Tom P