Gráfico de Red con D3.js en Canvas

Una completa guía paso a paso para generar gráficos de Red con D3.js

Ruben Triviño
DotTech
8 min readMay 5, 2020

--

  1. Cómo usar D3 con WebComponents.
  2. Gráfico de Red con D3.js en Canvas.

Introducción

La visualización de datos es un prisma de muchas caras; donde unos ven un simple gráfico de barras otros ven una clara tendencia de crecimiento, un objetivo cumplido o incluso un simple .json de datos sin contexto.

Por ello, existen gran cantidad de soluciones para la graficación de los datos; desde aplicaciones enterprise (Tableau, QlikView, Carto, Datastudio) hasta herramientas de desarrollo propio (bokeh, Seaborn, d3.js). Lo cierto es que cada situación requiere una solución diferente, y muchas ofrecen más que suficiente, no hay por qué reinventar la rueda si no es necesario pero… Si estás aquí es porque necesitas hacerlo tú y crear una visualización de datos específica para compartir una reflexión o un análisis con los tuyos, de forma única.

Como ya introducía en otra historia, D3 es una de las mejores bibliotecas que te pueden ayudar a crear un gráfico a medida y más aún si operas en un entorno web.

Planteamiento del problema: Gráficos de Red

En esta ocasión, traigo un ejemplo de gráfico de red utilizando el elemento Canvas de HTML. El elemento Canvas permite dibujar el contenido sin necesidad de almacenar en el DOM todos los elementos visuales, como es el caso de utilizar un SVG para la graficación. Esto permite crear gráficos de gran volumen de elementos, sin incurrir en lentitud o pérdida de fluidez por el proceso de renderizado del navegador.

El gráfico que vamos a ver es un diagrama de nodos unidos mediante líneas que representan la relación entre ellos. Los datos que utilizo son de la empresa BuscoExtra en la que trabajo como COO y DataAnalyst. Esta estructura de datos almacena los distintos contratos de puesta a disposición que se han realizado en un periodo de tiempo entre establecimientos y trabajadores del sector de la hostelería (las líneas).

De esta forma, existen dos tipos de nodos: Extra y Establecimiento. Utilizaré dos colores diferentes para distinguir los tipos de nodos y además emplearé un radio más pequeño para los nodos de tipo Extra, ya que aparecen en mucha mayor cantidad.

Definiendo las interfaces

Los nodos de tipo Establecimientotienen un radio variable en función del número de extras diferentes que ha contratado en el periodo, de forma que: los nodos más pequeños son aquellos que han contratado pocos trabajadores y los nodos mas grandes son los que han contratado a mayor número de personas o nodos Extra.

Además, como un contrato entre un Extray un Establecimiento se puede repetir varias veces, he utilizado la variable interactions para identificar esto. De forma que, aquellas relaciones entre nodos que más veces se hayan repetido, la línea que los une será más gruesa.

Ejemplo de Nodos de distinto tamaño y conexiones diferentes
Ejemplo de Nodos con diferentes Conexiones. Elaboración propia.

Todo este tipo de parámetros gráficos representan en realidad dimensiones diferentes y es lo que enriquece al gráfico. Alguien dijo alguna vez que un gráfico (imagen) transmite más que 1000 palabras, y yo también lo creo.

En otra estructura de datos podríamos utilizar una gama de colores para representar los nodos en función de algún otro criterio, utilizar distintas formas según la tipología de nodos (cuadrados, triángulos, etc..) o mejor aún, cambiar el tipo de línea que une los nodos en función del tipo de conexión: línea discontinua, línea puntuada, distinto color.

Podríamos utilizar una gran variedad de propiedades para disponer la información y añadir dimensiones adicionales, como hacen algunos con Spotify para mostrar los estilos de música, representando en polos opuestos los estilos más distantes. Las posibilidades son sencillamente infinitas, limitadas exclusivamente por nuestra creatividad.

Gráfico de Red de Artistas en Spotify según el número de fans. Fuente: Reddit: u/Enguzelharf

Getting Started

En primer lugar creamos un proyecto base en Stencil y facilitamos la estructura de datos en los parámetros de entrada. Te dejo la referencia de cómo hacerlo, súper rápido.

Una vez disponemos de los datos en el componente, empezamos a plantear la estrategia de dibujado. A diferencia del SVG, en Canvas utilizaremos las propiedades de un pincel: grosor del pincel, color, etc.. Además, tendremos que ir actualizando la posición del pincel en el lienzo a medida que vayamos dibujando. Por lo tanto, en Canvas no existen los elementos que pintamos, existe lo que llevamos pintado, mientras que en SVG los elementos los situamos mediante las propiedades de etiquetas <g>, <image>, <text>, etc..

Canvas ofrece operaciones de pintado para realizar geometrías básicas: líneas, círculos, arcos, cuadrados, etc. Con estos elementos, lo podemos hacer… ¡TODO! Pero tranquilo, si lo piensas, para este gráfico solo tenemos que dibujar círculos y líneas ¿Fácil verdad? Lo es.

Dibujando los Nodos

En el siguiente fragmento de código se muestran las operaciones necesarias para dibujar un nodo.

Dibujando un nodo

En esta función ocurren tres cosas principalmente: se inicia el trazo de dibujo, se configura los parámetros del pincel (color, ancho, etc..) y se realiza el trazado.

Todas las operaciones sobre el Canvas se realizan sobre su contexto, referencia que obtenemos al seleccionar el elemento HTML con D3.js.

  • beginPath()
    Esta función es el símil de apoyar el pincel sobre el lienzo y nos permite generar un trazo en las siguientes operaciones de movimiento del pincel.
  • lineWidth, fillStyle y strokeStyle
    Estas propiedades nos permiten configurar el tipo de trazado, en función del tipo de nodo, utilizaremos un color u otro. En este caso, vamos a trazar un círculo con un trazado invisible, rellenando el área interior de un color homogéneo.
  • moveTo(x,y)
    Con esta operación posicionamos el pincel allí donde queremos empezar a pintar. En este caso, llevaremos el pincel a la posición del nodo indicando los parámetros X e Y del mismo, donde dibujaremos un círculo.
  • arc(x,y,radius,originalAngle, finalAngle)
    Esta función nos permite realizar un trazado siguiendo un arco en sentido horario, con origen en el nodo, con el radio que establezcamos para cada tipo de nodo y desde el ángulo 0 a 2π (un círculo completo).
  • fill()
    Con esta función rellenamos la silueta con el color indicado en fillStyle previamente.
  • stroke()
    Esta función es la encargada de dibujar la línea del contorno de la figura.

Dibujando los Enlaces

En el siguiente fragmento de código se muestran las operaciones necesarias para dibujar una línea:

Función para dibujar una línea

Esta operación es incluso más sencilla, aparte de la configuración del pincel, inicio y finalizado del trazado. La única operación diferente que se realiza en la de unir dos puntos con una línea utilizando la función lineTo(x,y) .

  • lineTo(x,y)
    Esta función nos permite trazar una línea con origen en la posición actual del pincel y destino las coordenadas X e Y pasadas a la misma. Esto es, nos movemos la posición del nodo source y hacemos la línea hasta el nodo target. En este caso, el ancho de la línea lo condicionamos a una escala para ensancharla a medida que el número de interacciones es mayor.

Operación de Dibujado Completa

Una vez tenemos las funciones para dibujar los nodos y las líneas podemos definir la operación general de dibujo:

Función de dibujo

En esta función invocamos a los métodos de dibujo de los nodos y enlaces por cada uno de ellos. Además, con las funciones clearRect, save y restore limpiamos el lienzo y lo restauramos en cada iteración, para que las operaciones anteriores no se vayan acumulando.

Recuerda, hasta el momento no hemos utilizado D3.js, sólo hemos creado la lógica para dibujar nuestros elementos. Con D3 obtendremos la información que nos falta: la posición de los elementos.

Posicionando los Elementos

Todas las operaciones de pintado hacen referencia a la posición en la que se encuentran los nodos. Sin embargo, la estructura de datos inicial solo contiene referencias entre nodos, número de iteraciones, relaciones, etc., y no contiene ninguna información sobre la posición de los elementos.

Para iniciar la posición de los elementos utilizaremos D3, en concreto la función forceSimulation (referencia completa). Esta función realiza una simulación en la que va actualizando la posición de los elementos y nos modifica la estructura de datos original, de forma que tendremos a nuestra disposición los valores de X e Y de cada nodo.

Añadiendo la simulación

forceSimulation realiza una simulación de fuerzas a partir de un listado de nodos. Inicialmente nuestros nodos no tienen posición y éste es el encargado de establecer los valores de X e Y de cada nodo en cada iteración. Por esto, en el GIF del artículo se ve como una explosión de nodos desde el centro, es el inicio de la simulación.

Esta función nos permite indicar un listado de nodos, declarar una serie de fuerzas y suscribirnos a cada iteración de la simulación, donde tendremos las posiciones actualizadas de los nodos, lo que nos permitirá dibujar. Las fuerzas que podemos declarar son:

  • link
    Son fuerzas de unión entre nodos, nos permite establecer una restricción de distancia entre los nodos a partir de la función de D3 forceLink a la que pasamos nuestro listado de enlaces y la distancia deseada entre nodos.
  • charge
    Esta fuerza crea una mecánica similar al de la carga de los electrones por cada nodo. Entre las opciones que nos ofrece D3 utilizamos forceManyBody con una fuerza negativa, lo que establece un campo de fuerzas como la gravedad pero de sentido opuesto. Esto es, todos los nodos se repelen entre si y esta disminuye con la distancia hasta llegar a cero cuando se alcanza la distancia distanceMax que indicamos en la función.
  • center
    Por otro lado, podemos establecer un campo de fuerzas radial para mantener todos los grafos unidos y que los nodos aislados no se dispersen demasiado. Para ello, establecemos una fuerza que atrae a todos los nodos por igual hacia el centro del lienzo.

La función tick es invocada en cada iteración de la simulación y podemos aprovechar para dibujar nuestros nodos en cada una de ellas, consiguiendo así un efecto de animación. Pero si lo preferimos, podemos esperar a que la simulación finalice para dibujar nuestros datos, utilizando en este caso la función end. En este caso, utilizaremos la función tick ya que la aprovecharemos para hacer nuestro gráfico interactivo.

Por qué Canvas frente a SVG

El elemento Canvas de HTML introduce una serie de complejidades al no disponer referencia de los elementos dibujados, teniendo que realizar operaciones iterativas sobre los datos y almacenar la posición de los mismos para poder actualizar su estado. Sin embargo, la estructura de datos existe exclusivamente en el VirtualDOM, a diferencia de SVG, cuya estructura de datos es “renderizada” en el DOM y redibujada ante cualquier interacción del usuario con los elementos. Esto produce una reducción importante en las operaciones de pintado, alcanzando su máximo beneficio cuando se opera sobre miles o cientos de miles de elementos visuales. Sin embargo, cuando la superficie a pintar es muy amplia, Canvas pierde abrumadoramente frente al SVG.

No siempre se están mostrando miles de puntos de datos y un procesado previo previo a la visualización de los datos puede realizarse en el servidor o incluso en local. Sin embargo, en ocasiones es inevitable. En estos casos el elemento Canvas puede ser una solución cuando el SVG sencillamente deja colgado el navegador.

Esto no es un tema cerrado, os dejo algunas referencias que hablan sobre el rendimiento y sobre otras virtudes y desventajas entre SVG y Canvas.

Conclusiones

Si te interesa ver un proyecto completo con funcionalidades interactivas, implementado tanto en SVG como en Canvas, te dejo referencia al github donde está subido. Incluyo además los archivos del dataset para que explores la estructura de datos o incluso puedas sustituirla por la tuya propia.

--

--