Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I create a <dl> using d3.js

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.

like image 605
Matthew Avatar asked May 01 '18 19:05

Matthew


People also ask

What format does D3 use to create graphics?

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.

How can we use D3 js explain with example?

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.

Do people still use D3 js?

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.

How do I import a D3 file into JavaScript?

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.


2 Answers

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>
like image 79
Andrew Reid Avatar answered Sep 18 '22 05:09

Andrew Reid


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, ...).

like image 21
Xavier Guihot Avatar answered Sep 19 '22 05:09

Xavier Guihot