3

Consider this simple block of HTML:

<div id="somediv">
    <btn id="somebutton">Abc</btn>
    <div class="nested">
        <span id="span1">Blah blah blah</span>
        <span id="span2">Bleh bleh bleh</span>
    </div>
</div>

I'd like to recreate it, dynamically, using D3JS.

This works fine for this purpose:

(function() {

    var somediv = d3.select( 'body' ).append( 'div' ).attr( 'id', 'somediv' );

    somediv.append( 'btn' )
            .attr( 'id', 'somebutton' )
            .html( 'Abc' );

    var nested = somediv.append( 'div' )
        .attr( 'class', 'nested' );

    nested.append( 'span' ).attr( 'id', 'span1' ).html( 'Blah blah blah' );
    nested.append( 'span' ).attr( 'id', 'span2' ).html( 'Bleh bleh bleh' );

})();

…however, it's very verbose, with four commands and two temporary variables… Most of all, it's not easy to read – don't like it.

I'm looking for a way to do it in a single command.

So far, I have this:

    d3.select( 'body' )
        .append( 'div' )
            .attr( 'id', 'somediv' )
        .append( 'btn' )
            .attr( 'id', 'somebutton' )
            .html( 'Abc' )
        .append( 'div' )
            .attr( 'class', 'nested' )
            .append( 'span' ).attr( 'id', 'span1' ).html( 'Blah blah blah' )
            .append( 'span' ).attr( 'id', 'span2' ).html( 'Bleh bleh bleh' );

…however (as is to be expected), this results in constant nesting (i.e.: span2 is in span1, which is in div which is in btn which is in #somediv.)

Is there something in the D3 JS API that will let me return (to) the parent element to continue appending from it?

Fabien Snauwaert
  • 4,995
  • 5
  • 52
  • 70

2 Answers2

5

You could do this in a single command, but it would likely become less readable than the multiple commands you have. For example:

d3.select("body")
  .append("div")
  .each(function() {
    d3.select(this).append("button").html("button")
    d3.select(this).append("div")
      .each(function() {
         d3.select(this).append("span").text("span")
         d3.select(this).append("span").text("span")
      })
  })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

This certainly works, as you can see, but it might not be considered as readable because it is an unusual d3 pattern, especially if you need to share the code with others that are familiar with d3. The readability would likely degrade as we nest further too.

You can also use a method I suggested in a previous answer, and create a selection.appendSibling method. With the extra verbosity of creating the method, you can get something that looks a bit more clean:

// modify d3.selection so that we can append a sibling
d3.selection.prototype.appendSibling = function(type) {
  var siblings =  this.nodes().map(function(n) {
    return n.parentNode.insertBefore(document.createElement(type), n.nextSibling);
  })
  return d3.selectAll(siblings).data(this.data());
}

d3.select("body")
  .append("div")
  .append("button").html("button")
  .appendSibling("div") // append a sibling div to the button
  .append("span").text("span1")
  .appendSibling("span").text("span2"); // append a sibling span to the first span
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

But I don't suggest this approach - it is not clear what selection is being manipulated on each line. It was intended to be used when working with paired elements, such as dl and dd elements.


While the above two work, and you could also use an enter cycle to append the two spans rather than independent append statements, I still suggest breaking the code into multiple segments as you had originally used:

var container = d3.select("body").append("div");

var button = container.append("button").html("button");

var spans = container.append("div")
  .selectAll(null)
  .data([1,2])
  .enter()
  .append("span")
  .text(function(d) { return "span"+ d; })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

(of course, we could skip the var declarations for both spans and button to keep things tighter)

Or, more in keeping with the first two snippets:

var container = d3.select("body").append("div");
var button = container.append("button").html("button");
var div = container.append("div");
var span1 = div.append("span").text("span1")
var span2 = div.append("span").text("span2");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

(Again, I could remove the unnecessary var declarations if it is considered noise)

These last two snippets are actually shorter than the first one.

While this strays from the question, I believe these last two snippets are favorable for several reasons:

  • It allows easy manipulation of the independent elements - say you want an event listener on the button, or change it afterwards
  • It is clear what selection is being manipulated
  • It is more familiar/easy to understand to other readers/colleagues.
Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
  • Nice answer, again! I just fiddled around with inserting a `.select(function() { return this.parentNode; })`, which, of course, works, but there is definitely some code-smell to it. I decided to not post my answer for the same reasons you mentioned: it is highly unfimiliar to anyone used to D3, it breaks data binding and it obscures the selection in use. – altocumulus May 23 '18 at 15:52
  • @altocumulus, Thanks, I didn't even think of that option - though I figured there might be some other relatively simple alternatives - but yeah, I'd be pretty puzzled if I saw it in the wild so to speak (as with my initial option). – Andrew Reid May 23 '18 at 15:56
0

I like Andrew Reid's answer better but you can also use this.parentNode to append multiple elements to the same parents.

I am also using the enter() function with an array of length 2 to create the span elements with dynamic ids as well. Change the array length to create as many elements as you want.

var body = d3.select('body')
  .append('div').attr('id', 'someDiv')
  .append('button').attr('id', 'someBtn').html('Abc')
  .each(function() {
    d3.select(this.parentNode)
      .append('div')
      .selectAll('span').data([0, 1]).enter().append('span').attr('id', function(d, i) {
        return 'span' + (i + 1)
      }).html('Bleh Bleh Bleh')
  })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Aditya
  • 1,357
  • 1
  • 9
  • 19