6

I'd like to create a series of dl tags in a list from some data using d3.js.

The code I came up with is this:

var x=d3.select("body")
    .append('ol')
    .selectAll('li')
    .data(data)
    .enter()
    .append('li')
    .append('dl')
    .selectAll()
    .data(d=>Object.entries(d.volumeInfo)).enter();

x.append('dt')
    .text(d=>d[0]);

x.append('dd')
    .text(d=>d[1]);

where data is an array of objects. Everything works except the elements are not in the correct order.

Here is the order I manage to get:

<dl>
    <dt>key1</dt>
    <dt>key2</dt>
    <dd>value1</dd>
    <dd>value2</dd>
</dl>

But it should be like this:

<dl>
    <dt>key1</dt>
    <dd>value1</dd>
    <dt>key2</dt>
    <dd>value2</dd>
</dl>

I've done a fair amount of googling and nothing answers the question, at least not in a way that works in v5 or not with more than one dt/dd pair.

This seems like something basic that d3.js should be able to do.

Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
Matthew
  • 4,149
  • 2
  • 26
  • 53
  • what's in `data`? also, wouldn't `append('dl').selectAll().data(...)` set up a weird situation? Can you make a `let y = ... ` that is your `
    `, and then append the dt/dd elements to that first, then only once that's done, append it to `x`?
    – Mike 'Pomax' Kamermans May 01 '18 at 20:58

3 Answers3

5

In your solution:

x.append('dt')
    .text(d=>d[0]);

x.append('dd')
    .text(d=>d[1]);

All elements appended with an enter().append() cycle are appended to the parent, in the order they are appended, which for you runs like this: first all the dts, then all the dds, as you have seen. The placeholder nodes (these are not the appended elements) created by the enter statement do not nest children in a manner it appears you might expect them to.

Despite the fact that d3 doesn't include methods to achieve what you are looking for with methods as easy as a simple selection.append() method, the desired behavior can be achieved fairly easily with standard d3 methods and an extra step or two. Alternatively, we can build that functionality into d3.selection ourselves.

For my answer I'll finish with an example that uses your data structure and enter pattern, but to start I'll simplify the nesting here a bit - rather than a nested append I'm just demonstrating several possible methods for appending ordered siblings. To start I've also simplified the data structure, but the principle remains the same.


The first method might be the most straightforward: using a selection.each() function. With the enter selection (either with a parent or the entered placeholders), use the each method to append two separate elements:

var data = [
{name:"a",description:"The first letter"},
{name:"b",description:"The second letter"}
];


d3.select("body")
  .selectAll(null)
  .data(data)
  .enter()
  .each(function(d) {
 var selection = d3.select(this);
 
 // append siblings:
 selection.append("dt")
   .html(function(d) { return d.name; });
 selection.append("dd")
   .html(function(d) { return d.description; })
    
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

But, perhaps a more elegant option is to dig into d3.selection() and toy with it to give us some new behaivor. Below I've added a selection.appendSibling() method which lets you append a paired sibling element immediately below each item in a selection:

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());
}

It takes each node in a selection, creates a new paired sibling node (each one immediately after the original node in the DOM) of a specified type, and then places the new nodes in a d3 selection and binds the data. This allows you to chain methods onto it to style the element etc and gives you access to the bound datum. See it in action below:

// 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());
}

var data = [
    {name:"a",description:"The first letter"},
    {name:"b",description:"The second letter"}
    ];

d3.select("body")
  .selectAll(null)
  .data(data)
  .enter()
  .append("dt")
  .html(function(d) { return d.name; })
  .appendSibling("dd")  // append siblings
  .html(function(d) { return d.description; })  // modify the siblings
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

Of course it is probably wise to keep the siblings in separate selections so you can manage each one for updates/entering/exiting etc.

This method is very easily applied to your example, here's a nested solution using data that is structured like you expect and the appendSibling method:

// 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());
}

var data = [
 {volumeInfo: {"a":1,"b":2,"c":3}},
 {volumeInfo: {"α":1,"β":2}}
]

var items = d3.select("body")
    .append('ol')
    .selectAll('li')
    .data(data)
    .enter()
    .append('li')
    .append('dl')
    .selectAll()
    .data(d=>Object.entries(d.volumeInfo)).enter();
  
var dt = items.append("dt")
  .text(function(d) { return d[0]; })
  
var dd = dt.appendSibling("dd")
  .text(function(d) { return d[1]; })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
1

Here is a possibility using the .html appender (instead of .append):

var data = [
  { "volumeInfo": { "key1": "value1", "key2": "value2" }, "some": "thing" },
  { "volumeInfo": { "key3": "value3", "key4": "value4", "key5": "value5" } }
];

d3.select("body")
  .append('ol')
  .selectAll('li')
  .data(data)
  .enter()
  .append('li')
  .append('dl')
  .html( function(d) {
    // Produces: <dt>key1</dt><dd>value1</dd><dt>key2</dt><dd>value2</dd>
    return Object.entries(d.volumeInfo).map(r => "<dt>" + r[0] + "</dt><dd>" + r[1] + "</dd>").join("");
  });
dt { float: left; width: 100px; }
dd { margin-left: 100px; }
<script src="https://d3js.org/d3.v5.min.js"></script>

which produces this tree:

<ol>
  <li>
    <dl>
      <dt>key1</dt>
      <dd>value1</dd>
      <dt>key2</dt>
      <dd>value2</dd>
    </dl>
  </li>
  <li>
    <dl>
      <dt>key3</dt>
      ...
    </dl>
  </li>
</ol>

Note that this is not exactly in the spirit of d3 and makes it difficult to work with appended children (adding class, style, other children, ...).

Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
0

Example for data would be useful. But instead of appending to x, have you tried appending directly to dt itself?

x.append('dt').text(d=>d[0]).append('dd')

zerotimer
  • 91
  • 6