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.
D3 uses SVG to create and modify the graphical elements of the visualization. Because SVG has a structured form, D3 can make stylistic and attribute changes to the shapes being drawn.
D3 allows you to bind arbitrary data to a Document Object Model (DOM), and then apply data-driven transformations to the document. For example, you can use D3 to generate an HTML table from an array of numbers. Or, use the same data to create an interactive SVG bar chart with smooth transitions and interaction.
The JavaScript ecosystem has completely changed during this time, in terms of libraries, best practices and even language features. Nevertheless, D3 is still here. And it's more popular than ever.
Install D3 by running npm install d3 --save . Import D3 to App. js by adding import * as d3 from d3 . You need to use import * (“import everything”) since D3 has no default exported module.
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 dt
s, then all the dd
s, 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>
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, ...).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With