El join de datos: enter, update y exit
El patrón que hizo famoso a D3, qué problema resolvía en 2011, la función de clave y la constancia de objeto, y por qué en 2026 tu framework ya hace ese trabajo.
El problema que resolvía
En 2011 no había DOM virtual. Actualizar un gráfico cuando cambiaban los datos tenía dos salidas, y las dos eran malas.
Borrar y reconstruir. contenedor.innerHTML = '' y volver a pintar. Simple, correcto y destructivo: se pierde el foco del teclado, se pierde la posición del scroll, se cancela cualquier transición en curso, y sobre todo se pierde la identidad de los elementos. Un elemento que existía antes y sigue existiendo después es un nodo nuevo, así que el navegador no puede animar su cambio: no sabe que es el mismo.
Escribir el diff a mano. Recorrer los datos nuevos, buscar el nodo correspondiente, crear los que faltan, actualizar los que están y borrar los que sobran. Correcto y tedioso, y ese bucle se reescribe entero cada vez que cambia la estructura de los datos.
El join es la tercera salida: una operación que empareja un array de datos con un conjunto de nodos y te entrega las tres diferencias ya calculadas.
const seleccion = svg.selectAll('rect').data(datos, d => d.id);Esa llamada divide el mundo en tres:
- update: la propia
seleccion, los datos que ya tenían nodo. Hay que actualizarlos. - enter:
seleccion.enter(), los datos sin nodo. Hay que crearlos. - exit:
seleccion.exit(), los nodos sin dato. Hay que quitarlos.
Es exactamente la conciliación que hace un DOM virtual, expuesta como tres colecciones en lugar de escondida detrás de un render. Y la palabra join no es casual: es el join de una base de datos entre la tabla de los datos y la tabla de los nodos, con la función de clave haciendo de condición de igualdad.
La forma antigua y la moderna
El patrón clásico, el que aparece en todos los ejemplos anteriores a 2019:
const barras = svg.selectAll('rect').data(datos, d => d.id);barras.exit().remove();barras.enter() .append('rect') .attr('fill', '#89b4fa') .merge(barras) // union de enter y update .attr('x', d => x(d.mes)) .attr('y', d => y(d.valor)) .attr('width', x.bandwidth()) .attr('height', d => alto - y(d.valor));El merge es la pieza que confunde a todo el mundo: los atributos que solo se ponen al crear van antes, los que hay que actualizar siempre van después. Olvidar el merge produce un gráfico que se dibuja bien la primera vez y no se mueve nunca más, que es el bug número uno de D3.
Desde la versión 5.8 hay una forma mejor:
svg.selectAll('rect') .data(datos, d => d.id) .join('rect') .attr('x', d => x(d.mes)) .attr('y', d => y(d.valor)) .attr('width', x.bandwidth()) .attr('height', d => alto - y(d.valor)) .attr('fill', '#89b4fa');join hace las tres cosas: crea los que faltan, quita los que sobran y devuelve la unión de entrantes y actualizados. La versión de tres funciones da control cuando hace falta:
svg.selectAll('rect') .data(datos, d => d.id) .join( entra => entra.append('rect') .attr('y', alto) // nacen desde la linea base .attr('height', 0), actualiza => actualiza, sale => sale.transition().duration(300) .attr('height', 0) .attr('y', alto) .remove() ) .transition().duration(300) .attr('x', d => x(d.mes)) .attr('y', d => y(d.valor)) .attr('height', d => alto - y(d.valor));Ahí está el valor real del patrón: las tres transiciones son distintas. Lo que entra crece desde la base, lo que sale se encoge hacia la base, y lo que se queda se desliza a su nueva posición. Con un borrado y reconstrucción, las tres serían la misma: aparecer de la nada.
La clave y la constancia de objeto
El segundo argumento de data es la función de clave, y omitirla es el error más consecuente de todo el patrón.
Sin clave, D3 empareja por índice: el dato 0 con el nodo 0, el dato 1 con el nodo 1. Mientras los datos solo se añadan al final, funciona. En cuanto se borra un elemento del medio, o se reordenan, cada dato se ata a un nodo que representaba otra cosa.
// datos antes: [A, B, C, D] -> cuatro barras// datos despues:[A, C, D] -> se borra B// Sin clave: el nodo de B pasa a representar a C,// el de C a D, y el de D se va al exit.// Con clave: el nodo de B se va al exit,// los de C y D no se tocan.Visualmente la diferencia es enorme. Sin clave, borrar la segunda barra de cuatro produce una animación en la que las barras segunda, tercera y cuarta cambian de altura a la vez y desaparece la última: el ojo ve «los datos han cambiado». Con clave, se ve exactamente lo que ha pasado: la segunda barra se encoge y desaparece, y las demás se desplazan.
Eso es la constancia de objeto: que el mismo dato esté representado por el mismo elemento gráfico a lo largo del tiempo, de modo que el lector pueda seguirlo con la vista. Heer y Robertson lo estudiaron experimentalmente en 2007, en un trabajo sobre transiciones animadas en gráficos estadísticos, y midieron que las transiciones que preservan la identidad de los objetos mejoran de forma significativa la capacidad de los sujetos para seguir cambios en los datos frente a los cambios instantáneos.
La regla es corta: pon siempre una función de clave, y que devuelva un identificador estable del dato, no su posición.
.data(datos, d => d.id) // bien.data(datos, (d, i) => i) // es lo mismo que no poner nada.data(datos, d => d.nombre) // bien si los nombres son unicos y estables.data(datos, d => JSON.stringify(d)) // mal: cambia cuando cambia el valorLa última merece explicación: si la clave incluye el valor, un dato que cambia de valor se convierte en un dato distinto, así que sale por exit y entra por enter. El resultado es que nada se anima nunca, porque cada actualización es un reemplazo completo.
Y esto no es folclore de D3: es exactamente el atributo key de React, keyed each de Svelte y v-for con :key de Vue. El mismo problema, la misma solución, la misma trampa del índice. Si has entendido el join, has entendido por qué tu framework te avisa cuando falta la clave.