3

I have data like this:

[
  {id: 1, x:10, y:20, a: 123},
  {id: 2, x:20, y:12},
]

I want generate DOM to look something like this:

<div style='top:20px; left: 10px'>
  <strong>1</strong>
  <span>123</span>
</div>
<div style='top:12px; left: 20px'>
  <strong>2</strong>
</div>

Is there a way to append elements based on the existence of attributes?

Also a way to remove them if a new version of the data is missing those attributes?

1 Answer 1

4

Yes, you can use subselections.

Here is a function to generate the DOM you want:

function show(arr) {
    //bind data to divs
    parent = d3.select("body").selectAll("div")
        .data(arr);

    //enter-update-exit pattern used to create divs
    parent.exit().remove(); 
    parent.enter().append("div");
    parent.style({
        "top" : function (d) { return d.y} , 
        "left" : function (d) { return d.x } 
    });

    //basic subselection bind
    strong = parent.selectAll("strong")
        .data(function(d) { return [d.id]});

    //enter-update-exit 
    strong.exit().remove(); 
    strong.enter().append("strong")
    strong.text(String)

    // subselection bind that handles missing attributes      
    span = parent.selectAll("span")
        .data(function(d) { return d.a ? [d.a] : [] });

    //enter-update-exit 
    span.exit().remove();   
    span.enter().append("span")
    span.text(String)
}

My first call to show()

data1 = [
  {id: 1, x:10, y:20, a: 123},
  {id: 2, x:20, y:12},
  {id: 3, x:15, y:15},
];
show(data1);

appends this to the DOM:

<div style="top: 20px; left: 10px;">
    <strong>1</strong>
    <span>123</span>
</div>
<div style="top: 12px; left: 20px;">
    <strong>2</strong>
</div>
<div style="top: 15px; left: 15px;">
    <strong>3</strong>
</div>

I then call it again with

data2 = [
  {id: 1, x:10, y:20},
  {id: 2, x:20, y:12, a: 123}
]
show(data2);

and my DOM elements become

<div style="top: 20px; left: 10px;">
    <strong>1</strong>
</div>
<div style="top: 12px; left: 20px;">
    <strong>2</strong>
    <span>123</span>
</div>

The block that binds data to the divs is straightforward. No comment required.

parent = d3.select("body").selectAll("div")
    .data(arr);

The blocks that create the elements (div, strong, span) all are a straightforward application of the enter-update-exit idiom in d3. We do the exit, then the enter, and the update last--that's a common variation on the pattern.

parent.exit().remove(); //exit
parent.enter().append("div"); //enter
parent.style({
        "top" : function (d) { return d.y} , 
        "left" : function (d) { return d.x } 
}); //update

The subselections are where the magic happens (see the documentation for selection.data). The function you pass to data will receive the element in the array that its parent div is bound to (e.g {id: 2, x:20, y:12}). The function returns whatever element we want to bind our subselection to (which must be an array). For the strong elements, we are just grabbing the id and wrapping it in an array.

// parent datum {id: 2, x:20, y:12} becomes child data [2]
strong = parent.selectAll("strong")
            .data(function(d) { return [d.id]});

For the span element, we wrap the value of the attribute in the array when it exists, and just return an empty array when it does not.

// parent datum {id: 1, x:10, y:20, a: 123} becomes [123] 
// parent datum {id: 2, x:20, y:12} becomes []
span = parent.selectAll("span")
        .data(function(d) { return d.a ? [d.a] : [] });
Sign up to request clarification or add additional context in comments.

1 Comment

Very good answer. Complete example (not just code fragments) AND an explanation of important parts.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.