Búsqueda de sitios web

javascript - ¿Cómo creo un <dl> usando d3.js?


Me gustaría crear una serie de etiquetas dl en una lista a partir de algunos datos usando d3.js.

El código que se me ocurrió es este:

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]);

donde data es un arreglo de objetos. Todo funciona excepto que los elementos no están en el orden correcto.

Aquí está el pedido que logré conseguir:

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

Pero debería ser así:

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

He hecho una cantidad considerable de búsquedas en Google y nada responde la pregunta, al menos no de una manera que funcione en v5 o no con más de un par dt/dd.

Esto parece algo básico que d3.js debería poder hacer.

Respuesta #1

En tu solución:

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

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

Todos los elementos añadidos con un ciclo enter().append() se añaden al elemento padre, en el orden en que se añaden, lo cual en tu caso funciona así: primero todos los dt, luego todos los dd, como has visto. Los nodos de marcador de posición (estos no son los elementos añadidos) creados por la declaración enter no anidan hijos de la manera que podría parecer que esperas.

A pesar de que d3 no incluye métodos para lograr lo que estás buscando con métodos tan fáciles como un simple método selection.append(), el comportamiento deseado se puede lograr bastante fácilmente con métodos estándar de d3 y uno o dos pasos adicionales. Alternativamente, podemos incorporar esa funcionalidad en d3.selection nosotros mismos.

Para mi respuesta terminaré con un ejemplo que utiliza tu estructura de datos y el patrón de entrada, pero para empezar simplificaré un poco la anidación aquí; en lugar de un append anidado, solo estoy demostrando varios métodos posibles para agregar hermanos ordenados. Para empezar, también he simplificado la estructura de datos, pero el principio sigue siendo el mismo.

El primer método podría ser el más sencillo: usar una función selection.each(). Con la selección de entrada (ya sea con un padre o con los marcadores de posición ingresados), usa el método each para agregar dos elementos separados:

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>

Pero, quizás una opción más elegante es profundizar en d3.selection() y experimentar con él para darnos algún nuevo comportamiento. A continuación, he agregado un método selection.appendSibling() que te permite agregar un elemento hermano emparejado inmediatamente debajo de cada elemento en una selección:

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

Toma cada nodo en una selección, crea un nuevo nodo hermano emparejado (cada uno inmediatamente después del nodo original en el DOM) de un tipo especificado, y luego coloca los nuevos nodos en una selección de d3 y enlaza los datos. Esto te permite encadenar métodos sobre él para estilizar el elemento, etc., y te da acceso al dato enlazado. Véalo en acción a continuación:

// 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>

Por supuesto, probablemente sea prudente mantener a los hermanos en selecciones separadas para que puedas gestionar a cada uno para actualizaciones/entradas/salidas, etc.

Este método se aplica muy fácilmente a tu ejemplo, aquí hay una solución anidada usando datos que están estructurados como esperas y el método appendSibling:

// 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>

Respuesta #2

Aquí hay una posibilidad usando el apéndice .html (en lugar de .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>

lo que produce este árbol:

<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>

Tenga en cuenta que esto no está exactamente en el espíritu de d3 y hace que sea difícil trabajar con los hijos añadidos (agregar clase, estilo, otros hijos, ...).