64

I would like to create multiple non-nested elements using d3 to create a structure like this:

    <div id="parent">
        <p> from data[0] </p>
        <p> from data[0] </p>

        <p> from data[1] </p>
        <p> from data[1] </p>

        <p> from data[2] </p>
        <p> from data[2] </p>
    </div>

creating nested structures would go something like

    d3.select('#parent').selectAll('p').data(data).enter().
           append('p')...append('p')

but I would like to maintain the original selection even after the append, so I could continue appending to the parent element. Thank you!

0

6 Answers 6

74

The idomatic way of doing is with nesting:

var divs = d3.select('#parent').selectAll('p').data(data).enter().append('div');

divs.append('p')
divs.append('p')

Which creates:

<div id="parent">
  <div>
    <p> from data[0] </p>
    <p> from data[0] </p>
  </div>

  <div>
    <p> from data[1] </p>
    <p> from data[1] </p>
  </div>

  <div>
    <p> from data[2] </p>
    <p> from data[2] </p>
  </div>
</div>

If that won't work, save your selection and repeatedly append:

var enterSelection = d3.select('#parent').selectAll('p').data(data).enter();

enterSelection.append('p')
enterSelection.append('p')

then sort what you've added:

d3.select('#parent').selectAll('p').sort(function(a, b){ return a.index - b.index; })

You'll need to add an index property to each element of data that describes the sort order. The normal i is only defined in the context of a particular selection, which is lost when we reselect.

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks Adam. I hadn't thought of just sorting afterward
Works perfectly, my data is going to be sorted by data, anyways. And just for later reference by anyone, we must reselect d3.select('#parent') in the last line to sort instead of using the enterSelection, as we already entered(). Thanks!
You can also use enterSelection.insert("p") for the second element. That should eliminate the need to sort as last step.
27

Use append() for the first item and insert() for the second. This eliminates any need to sort afterwards (thanks to @scuerda's comment for pointing this out) (JSFiddle):

data = [{}, {}, {}];
var enterSelection = d3.select('#parent').selectAll('p').data(data).enter()

enterSelection.append('p').text(function(d, i) {return 'from data[' + i + ']'})
enterSelection.insert('p').text(function(d, i) {return 'from data[' + i + ']'})

This will give you the exact structure requested:

<p>from data[0]</p>

<p>from data[0]</p>

<p>from data[1]</p>

<p>from data[1]</p>

<p>from data[2]</p>

<p>from data[2]</p>

4 Comments

This is actually the solution that should be the accepted one because it is exactly what was asked for, without any detours!
this is nice, but are you sure it is not : <p>from data[0]</p> <p>from data[1]</p> <p>from data[2]</p> <p>from data[0]</p> <p>from data[1]</p> <p>from data[2]</p>
It depends on the d3 version. For example, with d3 v7 it doesn't work anymore
Still to find a solution for newer versions of d3
20

You can also do this in a single select/enter cycle as follows

d3.select('#parent').selectAll('p').data(data).enter().
append('p').text(function(d) {return 'from data[0]')}).
select(function() { return this.parentNode; }).
append('p').text(function(d) {return 'from data[0]')});

2 Comments

this is gross and exactly what I'm looking for
Is this significantly faster in terms of CPU cycles than the approach where you capture a selection and then append to the selection twice?
9

Instead of a .append(),

You can also wrap a function that creates new content in a .html()

d3.select('#parent')
  .selectAll('div')
    .data(data)
  .enter()
    .append('div')
    .html(function(d) {return "<p>" + from data[0] + "<p>" etc..... ;});

Comments

1

Similar to the above but in a different idiom. This is truer to the nested selections approach and eliminates the need for sorting or insertion:

var divs = d3.select('#parent');

var ps = divs.selectAll('#parent > div')
    .data(d3.range(3)).enter().append('div');

ps.append('p').html(function(d,i) { return 'from data[' + i + ']'; });
ps.append('p').html(function(d,i) { return 'from data[' + i + ']'; });

More elegantly (and probably faster):

var divs = d3.select('#parent');

  var ps = divs.selectAll('#parent > div')
    .data(d3.range(3)).enter().append('div');

    ps.append('p').html(function(d,i) { return 'from data[' + i + ']'; })
    .select(function() { return this.parentNode.appendChild(this.cloneNode(true)); });

Comments

1

1. Duplicate each element

Create a new list, which contains all element twice (like this):

doubleData = data.reduce(function (res, current, index, array) {
    return res.concat([current, current]);
}, []);

d3.select('#parent').selectAll('p').data(doubleData).enter().append('p')

2. Enter twice

Sometimes you want to do further manipulation on the appended elements. In this case, you maybe want to know if you are accessing the first or the secondly added element. Add a first-paragraph and second-paragraph class to these elements.

d3.select('#parent').selectAll('.first-paragraph')
  .data(data)
  .enter()
  .append('p')
  .attr('class', 'first-paragraph');

d3.select('#parent').selectAll('.second-paragraph')
  .data(data)
  .enter()
  .append('p')
  .attr('class', 'second-paragraph');

In this case you maybe have to sort the elements based on Adam Pearce's answer.

Comments

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.