El patrón update de D3: empatando tus datos con elementos visuales

¿De qué se trata el update-pattern?

En resumen, el update-pattern es una metodología de D3 en la que podemos ligar piezas de información con elementos gráficos, para después manipular la apariencia de estos elementos.

Ya que los humanos somos seres visuales, nos conviene ser capaces de darle una representación gráfica a los datos. El gran poder del update-pattern radica en que puedes usar los datos ligados a los elementos HTML para modificar sus propiedades visuales; por ejemplo, su color, tamaño, posición, etc. Para esto, el update-pattern nos ofrece una manera de manipular al DOM; recuerda que el DOM es el objeto que mantiene un registro de todos elementos HTML activos en la ventana del navegador. D3 se encarga de mantener un registro de esta relación dato-elemento. También es posible cambiar qué dato está relacionado a qué elemento.

Al proceso mediante el cual ligamos datos a elementos de HTML le llamaremos proceso-join. Este es un concepto importante que estaremos usando a lo largo del post. Ya que D3 es una librería en JavaScript, las piezas de información serán usualmente objetos con el formato de JSON y los elementos visuales serán elementos de HTML, lo más probable, elementos SVG (Scalable Vector Graphics).

¿Tienes dudas sobre qué son los elementos SVG? en realidad es muy sencillo, simplemente son elementos de HTML con los cuales se dibujan figuras en el navegador; por ejemplo, rectángulos, círculos, triángulos, etc. Lo que es muy importante es que para poder visualizar estos elementos, es necesario que estén embebidos dentro de un elemento <svg>.

Los elementos que vamos a estar utilizando en este pequeño tutorial son: <rect> (un rectángulo) y <circle> (un círculo). A manera de ejemplo, te presento un fragmento de código para dibujar un cuadrado verde; con 50 pixeles de alto y ancho; y ubicado en la posición (0, 0) de nuestro lienzo (el elemento <svg>):

<svg>
    <rect x="0" y="0" width="50" height="50" fill="green"/>
</svg>

¿Qué te parece si conocemos más de esta versátil técnica?

Los estados de selección

Para entender cómo opera el update-pattern, imagina que tienes dos conjuntos: uno de datos y otro de elementos HTML. Después de ejecutar el proceso-join, podemos tener 3 posibles estados de selección: selección-enter, selección-update y selección-exit. Recuerda, lo que el proceso-join hace es ligar datos con elementos. En el siguiente diagrama podemos ver a éste proceso esquematizado:

En la selección-update tenemos a los elementos HTML que ya se encontraban presentes en la ventana del navegador y que se han podido ligar a algún dato. Si antes del proceso-join el conjunto de datos o el conjunto de elementos estaban vacíos, esta selección también estará vacía.

Cuando tenemos más datos que elementos HTML, estos datos “sobrantes”, que no pueden ser ligados a algún elemento, producen la selección-enter. Por el contrario, cuando todos los datos logran ser ligados a algún elemento HTML, esta selección está vacía. En realidad, en esta selección no hay elementos sino marcadores para los datos; sin embargo, como veremos posteriormente, podemos utilizar estos marcadores para manipular al DOM y crear nuevos elementos.

Finalmente, en los casos en que existen más elementos HTML que datos, los elementos “sobrantes” caerán en la selección-exit. Cuando la cantidad de elementos es menor o igual a la cantidad de datos, esta selección estará vacía. Usualmente, los elementos en esta selección son aquellos que serán removidos del DOM; y ¿por qué es esto? Recuerda que nuestro objetivo es crear una representación gráfica de los datos; si lo piensas un momento, no tendría sentido tener elementos gráficos si éstos no están representando a algún dato.

En seguida vamos a ver varios casos para que entiendas mejor cómo ejecutar el proceso-join y los estados de selección resultantes.

Presentación de los ejemplos

Casi todos los ejemplos que te estaré presentando tienen dos partes principales: Por un lado, un diagrama que esquematiza los estados de selección que ya hemos discutido; éstos variarán dependiendo de la manera en la que hallamos realizado el proceso-join.

Por otro lado, también te estaré presentando el código que hace la magia; debajo de él podrás observar un elemento <svg> con el resultado gráfico de haberlo ejecutado. Pero la mejor parte es que el código está en bloques interactivos, así que ¡puedes modificarlo y ver el resultado! Para hacer esto, basta con que cambies el contenido en el área de texto y que presiones el botón “ejecutar” que está debajo del código.

Con el botón “reiniciar” puedes devolver el bloque interactivo a su estado inicial; esto es, antes de que presionaras el botón ejecutar. Tanto el lienzo <svg> cómo el área con el código editable serán reiniciados. Es conveniente que presiones este botón antes de que cambies y ejecutes el código; de este modo, podrás observar exactamente el resultado de los cambios que hubieras realizado.

Profundizando en la selección-enter: Insertando nuevos elementos en un lienzo vacío

Imagina que tienen un conjunto de datos como el siguiente:

[{x:5, y:5}, {x:90, y:30}, {x:105, y:65}, {x:190, y:55}]

Como podrás notar, se trata de un arreglo con 4 objetos; cada objeto tiene dos propiedades: x y y. Podemos usar estos datos para designar las posiciones de 4 elementos HTML. Con el objetivo de crear estos elementos, usaremos el código que te presento a continuación (no olvides presionar el botón “ejecutar” para ver el resultado).

¿Qué es lo que está pasando?

Antes de ejecutar cualquier código, estamos creando un elemento <svg> con la id “enter-selection-demo-svg”. Este elemento es el que funcionará como un “lienzo” para dibujar nuestra visualización.

Ahora veamos el script; en la primera línea de código únicamente vamos a definir los datos que emplearemos. Posteriormente, creamos un objeto selección que hace referencia a nuestro lienzo <svg>.

Hasta este punto no hemos hecho nada extraordinario; sin embargo, ¡la diversión está por empezar! En la siguiente línea de código, en donde definimos al objeto update_selection, estamos uniendo el conjunto de datos con la selección de elementos <circle> que están adentro del lienzo <svg>; no obstante, ya que en este punto dicho elemento está vacío, la selección de círculos también estará vacía.

Y ¿de qué nos sirve una selección vacía? Una característica interesante de D3 es que puede ligar datos incluso a selecciones vacías; precisamente, eso es lo que estamos haciendo cuando invocamos el método .data(dataset). Este pequeño método es clave en el update-pattern; es él quien ejecuta el proceso-join y quien se encarga de ligar datos a elementos; así mismo, él está creando los tres estados de selección que ya hemos visto. El método .data(dataset) se invoca desde un objeto selección y debe recibir como argumento el conjunto de datos al que queremos ligar la selección de elementos HTML. Este método devuelve directamente un objeto update_selection, el cual nos permitirá acceder a los elementos en la selección-update.

En nuestro ejemplo, después de ejecutar el proceso-join, tendríamos los siguientes estados de selección:

Como te habrás percatado, tanto la selección selección-update como la selección-exit están vacías y solamente tenemos elementos en la selección-enter; no obstante, el método .data(dataset) únicamente nos da acceso a la selección-update; siendo así, ¿cómo podemos acceder, precisamente a la selección-enter? Afortunadamente, el objeto update_selection tiene métodos para acceder tanto a la selección-enter como a la selección-exit; para el primer caso, estaríamos empleando el método .enter(), el cual nos devuelve un objeto enter_selection. En nuestro ejemplo, la línea de código que nos permite acceder a la selección-enter es:

enter_selection = update_selection.enter();    

Ahora, quiero que prestes mucha atención a la siguientes líneas de código; éstas son las que están insertando los elementos <circle> en el lienzo <svg>

enter_selection.append("circle")
        .attr("fill","coral")
        .attr("cx", (d) => d.x)
        .attr("cy", (d) => d.y)
        .attr("r", (d, i) => (i + 1) * 5);

Como recordarás, nuestra selección-enter no contiene elementos sino marcadores; en nuestro ejemplo, tenemos 4 marcadores. Ahora es tiempo de convertir esos marcadores en elementos; esto es exactamente lo que hacemos con el método .append("circle"); por cada marcador en la selección-enter, estamos añadiendo un elemento <circle>. Mediante la invocación sucesiva del método .attr(), estamos ajustando los atributos de estos 4 elementos. En nuestro ejemplo, los atributos que estamos ajustando son el color, la posición horizontal, la posición vertical, y el radio del círculo, respectivamente.

Ahora quisiera hablarte un poco más del método .attr(). Este método puede recibir hasta dos argumentos. El primero es simplemente el nombre del atributo que queremos modificar. El segundo parámetro representa el valor que le asignaremos al atributo y puede tomar dos formas: una constante o una función. En el caso de que sea una función, ésta deberá regresar el valor que se le asignará al atributo. La función puede recibir hasta 3 argumentos: el dato actual (usualmente representado con una d), el índice del dato (i), y el grupo actual de nodos (nodes).

En el ejemplo, el atributo fill está siendo asignado con una constante; los atributos cx y cy se están asignando mediante una función que solo requiere el dato actual; y el atributo r se asigna con una función que requiere tanto el dato actual como el índice del mismo. Tal vez te preguntes a qué me refiero con “el dato actual”; pues bien, es el dato particular que está siendo ligado a cada uno de los elementos HTML durante el proceso-join. En nuestro ejemplo, cada uno de los cuatro objetos JSON dentro del arreglo dataset fungirá como el dato actual en un momento dado. Como ya habíamos visto, estos objetos tienen dos propiedades (x y y), cuyos valores hemos empleado para posicionar a los elementos <circle>.

¿Qué te parece? bastante genial ¿no lo crees? El update-pattern nos permite evitar el uso explícito de bloques de bucle; en su lugar, le estamos indicando a D3 qué es lo que debe hacer con cada uno de los elementos gráficos que están en la selección.

¿Qué te parece si seguimos explorando este útil patrón?

Profundizando en la selección-update: Actualizando los elementos gráficos con un nuevo conjunto de datos

Ahora imagina que tienes un lienzo <svg> que ya tiene elementos dentro de él. Tal vez lo que deseas es actualizar los atributos de estos elementos. En este caso, la selección-update nos sería de gran utilidad. Mira el bloque de código que te presento a continuación. Como verás, tenemos 4 círculos grises embebidos dentro de un elemento <svg>. Ahora, ¡ejecuta el código en JavaScript para ver lo que pasa!

¿Qué está sucediendo? El código es muy parecido al que vimos en el caso anterior, solo que en esta ocasión nuestra selección de círculos dentro del elemento <svg>, no estará vacía. Después de ejecutar el proceso-join, nuestro conjunto de selecciones luciría de la siguiente manera:

Esta vez, el conjunto de datos y el conjunto de elementos se traslapan por completo ya que tenemos cuatro datos ligados a cuatro elementos; como resultado, todos los elementos están en la selección-update; tanto la selección-enter como la selección-exit están vacías.

Ahora pasemos a la última línea del script. En este punto, el código te debería ser familiar. Puesto que nuestro conjunto de datos tiene el atributo color podemos usar este valor justamente para colorear los círculos. Muy sencillo ¿no es cierto?

Profundizando en la selección-exit: Eliminando elementos que no están ligados a algún dato

¿Qué pasa cuando el número de registros en nuestro conjunto de datos es más pequeño que el número de elementos HTML a los que los estamos ligando? En este caso tenemos elementos “sobrantes” que no están ayudando a representar gráficamente a algún dato. Usualmente, lo que queremos es deshacernos de esos elementos; aunque cabe aclarar, no tienes la obligación de hacerlo y puedes manipular este grupo de elementos como mejor te convenga.

Veamos nuestro código de ejemplo. Fíjate que en este caso tenemos cuatro círculos antes de ejecutar al proceso-join. Ahora, corre el código y ve lo que pasa.

En esta ocasión, el arreglo dataset sólo tiene dos datos; en realidad, no usaremos la información en estos datos para alterar a los elementos HTML; sólo los usaremos para crear a la selección-exit. Después de ejecutar el proceso-join con el método .data(dataset), encontraríamos que todos los datos están ligados a algún elemento HTML; sin embargo, no todos los elementos están ligados a un dato. El siguiente diagrama esquematiza los estados de selección:

En este momento, tenemos elementos tanto en la selección-update como en la selección-exit; sin embargo, por lo pronto vamos a ignorar a la primera y concentrarnos en la selección-exit. Para acceder a esta selección, empleamos el método .exit() del objeto update_selection. La última línea de código es muy simple, lo único que está haciendo es remover los elementos que se encuentran en la selección-exit; y ¿cuáles son esos elementos? Como vimos al inicio, únicamente tenemos dos datos, los cuales estamos ligando a los primeros dos elementos HTML, es decir, a los círculos verdes. Los círculos rojos, que no pueden ser ligados a algún dato, son los que caen en la selección-exit.

Ligando datos y elementos gráficos a través de una llave

Hasta este instante, hemos estado ligando los datos y los elementos en el orden en que aparecen; el primer dato con el primer elemento, el segundo dato con el segundo elemento, etc. No obstante, puede ser de gran utilidad ligar a los elementos con datos específicos, basándose no en la posición sino en alguna clave particular. Afortunadamente, D3 posee un mecanismo para hacer justo esto. Permíteme presentarte el código que haría el truco. Estamos partiendo de un lienzo <svg> que contiene cuatro elementos <circle>. He dividido el código de JavaScript en dos partes para entender mejor el proceso.

En la primera parte tan solo estamos ligando a los cuatro elementos <circle> con el conjunto de datos en la variable linkDataset. ¿Puedes imaginar qué clase de selección estamos creando? piénsalo un momento antes de continuar… ¡así es! sólo tendríamos elementos en la selección-update. De hecho, no vamos a hace nada con esta selección-update, únicamente estamos invocando al método .data(linkDataset) con el fin de ligar a los elementos HTML con un dato. En esta primera etapa los datos son ligados a los elementos en el orden en que aparecen, tal y como lo hemos visto hasta ahora.

Tal vez te preguntes cómo es que D3 mantiene la referencia de qué dato está ligado a cual elemento; pues bien, ¿recuerdas que todo lo que vemos en la ventana del navegador está registrado en el DOM? Lo que D3 hace es agregar una nueva propiedad llamada __data__ a cada uno de los objetos representados por los elementos gráficos. En el ejemplo, cada objeto círculo tendrá una propiedad __data__ cuyo valor es uno de los objetos JSON en el arreglo linkDataset; el objeto “alpha” para el primer círculo, el objeto “beta”, para el segundo círculo, etc. La ventaja de este mecanismo es que el elemento gráfico se mantendrá ligado al mismo dato hasta que volvamos a ejecutar el proceso-join; mientras tanto, no importa cuántas veces selecciones a los elementos HTML, ellos permanecerán ligados al mismo dato.

Exploremos la segunda parte del código. Esta vez, nuestro objetivo es cambiar los datos ligados al segundo y al cuarto círculo. Como te habrás dado cuenta, en esta ocasión nuestro conjunto de datos, llamado updateDataset, tiene únicamente dos componentes; usaremos la propiedad “keyLink” de los datos para ligarlos a los elementos HTML correctos, y la propiedad “color” nos servirá justamente para cambiar el color de esos elementos.

Pero, ¿cómo le decimos a D3 que queremos que use como referencia a la propiedad “keyLink” para ligar elementos HTML y datos, en lugar de basarse en la posición? Verás, el método .data(), de hecho, puede aceptar un segundo argumento llamado “key”: una función que, a su vez, recibirá como uno de sus argumentos al dato actual. Esta función debe regresar un valor que se usará para determinar con qué elemento debe ligarse al dato. D3 aplicará esta función tanto al nuevo dato como al dato actualmente ligado a cada uno de los elementos en la selección. Si la función regresa el mismo valor, el nuevo dato será ligado a ese elemento.

En nuestro ejemplo, la función “key” será aplicada a los datos en el arreglo updateDataset y a los datos ligados a los círculos; en ambos casos tomará el dato y regresará el valor de la propiedad “keyLink”. Los elementos y los datos que estén relacionados al mismo valor para la propiedad “keyLink” serán ligados; estos elementos caerán en la selección-update.

En la línea final del código, únicamente estamos alterando el color de los elementos encontrados en la selección-update.

Vale la pena mencionar que cuando insertamos elementos usando la selección-enter, D3 automáticamente ya está ligando estos elementos a los datos relacionados.

Trabajando con las 3 selecciones al mismo tiempo

Con todo lo que has aprendido, ya debes ser un as en el uso del update-pattern. Solo quisiera ahondar en una estrategia más antes de darte tu diploma de graduación. ¿Qué pasa cuando queremos manipular más de una selección al mismo tiempo? Por ejemplo, supón que queremos modificar los elementos presentes tanto en la selección-enter como en la selección-update. Por supuesto, podríamos manipular cada selección por separado y repetir el código común; no obstante, D3 tiene una mejor manera: el método .merge(other).

En el siguiente ejemplo vamos a realizar operaciones sobre los 3 estados de selección.

En este caso, el código también está dividido en dos etapas. En la primera etapa insertamos cuatro círculos; cada uno de ellos estará ligado a un dato del arreglo insertDataset. El valor de la propiedad keyVal nos servirá para identificar a cada uno de los objetos JSON dentro de los arreglos que estaremos usando; también, esta es la propiedad que nos servirá para ligar a los elementos HTML a un nuevo conjunto de datos. Cada círculo insertado en esta etapa será negro ya que no estamos indicando ningún color.

Ahora, exploremos la segunda etapa. Fíjate que en este caso los elementos en el arreglo updateDataset también tienen la propiedad keyVal. Del mismo modo, nota que en este nuevo arreglo de datos ya no tenemos objetos identificados con “a” o “b”; así mismo, ahora tenemos dos nuevos objetos, identificados como “e” y “f”. Una vez que invocamos el método .data(updateDataset, d => d.keyVal) (línea 2.6) tendremos elementos en las tres selecciones.

Antes de continuar, piensa por un momento qué objetos quedarán en qué selección. ¿Ya lo pensaste bien? En la selección-update tenemos los objetos identificados como “a” y “b”, pues tanto antes como después del proceso-join tenemos objetos identificados con estas claves; en la selección-enter tenemos los objetos identificados como “e” y “f”, ya que son datos “nuevos” que no se encontraban en el conjunto de datos anterior; finalmente, en la selección-exit se encuentran los objetos identificados con “c” y “d”, debido a que son objetos del anterior conjunto de datos que no tienen una contraparte en el nuevo.

Usando el método .remove() (línea 2.7), estamos eliminando todos los elementos de la selección-exit. Enseguida, usamos los marcadores de la selección-enter para insertar dos cuadrados y ajustar su apariencia (líneas 2.8 a 2.10).

El siguiente paso es algo que no habíamos hecho hasta ahora; estamos invocando al método .merge(update_selection) desde la selección-enter (línea 2.11). Este método es invocado desde un objeto selección y recibe un parámetro llamado “other”; este parámetro es a su vez un objeto selección. Lo que este método hace es crear un nuevo objeto selección basado en la selección que lo invoca (la selección-enter, en el ejemplo) y lo complementa con los elementos presentes en la selección “other” (la selección-update, en nuestro caso).

¡Cuidado! el método .merge(other) no está uniendo ambas selecciones, aunque el efecto es parecido; lo que está haciendo es complementar los elementos de la primera selección con los de la segunda. En caso de que ambas selecciones tuvieran elementos identificados de la misma manera, únicamente se tomaría en cuenta al elemento de la selección que invocó a este método.

Una vez que tenemos al nuevo objeto selección, podemos modificar simultáneamente tanto los elementos de la selección-enter como los de la selección-update; en nuestro caso, estamos ajustando el color, borde y grosor del borde (líneas 2.12 a 2.14).

Conclusiones

¿Qué te pareció? seguramente a estas alturas te has dado cuenta del poder y la versatilidad del update-pattern. También debes tener los elementos suficientes para empezar a usar esta herramienta en tus próximos proyectos.

Tal vez te preguntes ¿cuál es la diferencia entre el proceso-join y el update-pattern? Es muy simple, el proceso-join es el que usamos para ligar los datos a elementos gráficos; lo ejecutamos usando el método .data() del objeto selección. Por otro lado, el update-pattern es la ejecución del proceso-join, seguida de operaciones sobre la selección-enter, la selección-update, y/o la selección-exit.

Si aún tienes dudas de cómo opera D3 en general ¡Estas de suerte! el próximo post tocará precisamente este tema; así que, mantente en sintonía.