Gestión de los ejes de coordenadas con d3.js

Cuando quieres representar una serie de datos usando una gráfica lineal usando d3.js, uno de los puntos que más cuesta dominar es el mapeo de tus datos sobre el eje vertical. El motivo es que este mapeo de los datos no es directo. A ver si consigo explicarme usando unos diagramas:

Por lo general, nosotros queremos representar nuestros datos de este modo:

Gestion de ejes de coordenadas en d3 - Figura 1

Sin embargo, el sistema de coordenadas de nuestro objeto svg (que, a fin de cuentas, es lo que subyace por debajo de d3.js) es de este otro modo:

Gestion de ejes de coordenadas en d3 - Figura 2

Además, tenemos un inconveniente adicional: por lo general, no queremos mostrar nuestros datos justo en el borde del objeto svg sino que queremos dejar cierto margen entre el borde y nuestra gráfica:

Gestion de ejes de coordenadas en d3 - Figura 3

Por estos motivos, cualquier punto de nuestra gráfica tiene dos coordenadas:

  • Unas coordendas (x,y) dentro del dominio de los valores que queremos representar en la gráfica.
  • Unas coordenadas (a,b) dentro del contenedor svg donde estamos posicionando los puntos que representan estos valores.
Gestion de ejes de coordenadas en d3 - Figura 4

El mapeo de los valores en el eje horizontal (el valor x se mapea en la coordenada a del contenedor) es relativamente trivial: los valores estan correlacionados directamente (mayores valores del dominio están asociados a valores mayores de coordenadas horizontales) y sólo es necesario tener en cuenta tener el offset necesario para respetar el margen horizonal.

Los que son más complejos de mapear son los valores en el eje vertical (el valor y debe mapearse en la coordenada b). El motivo es porque la correlación es inversa (los valores menores del dominio se posicionan en coordenadas mayores del contenedor).

Para ayudarnos con este mapeo, d3.js nos proporciona una serie de herramientas:

  • Para poder dibujar nuestra gráfica dentro del contenedor dejando cierto margen entre la gráfica y el borde, la mejor estrategia es no dibujar directamente en nuestro contenedor svg sino insertar dentro de nuestro contenedor elementos g que desplazaremos para conseguir los márgenes que deseamos. Dentro de estos elementos g será donde realmente dibujemos nuestra gráfica. Como este elmento g tiene su propio origen de coordenadas, de este modo, contamos con un sistema de coordenadas donde nuestro origen (0,0) corresponde al margen superior izquierda.
  • Para mapear nuestros valores (los vamos a llamar «valores del dominio») sobre las coordenadas apropiadas del elemento g necesitaremos aplicar una función mapeadora llamada d3.scale. Esta función, será la encargada de a) recibir como input los valores del dominio y b) calcular como resultado las coordenadas adecuadas.

Para poder realizar un ejemplo práctico, he usado la gráfica lineal que desarrollé en esta entrada. He preparado un ejemplo que dibuja la gráfica original pero al mismo tiempo dibuja la misma gráfica invirtiendo el eje vertical (valores crecientes se representan con líneas descendentes) para poder comparar ambas implementaciones. El resultado final es el siguiente:

El código

Y, ahora, vamos a lo realmente interesante. Veamos el código completo:

Si comparamos el código con el articulo anterior creo que se nota que es muy similar. Sólo se han añadido los bloques necesarios para generar la «gráfica inversa». Pero gran parte del código es reutilizable. Vamos a analizarlo por partes:

  • Primero, creo una variable margins donde configuro los márgenes que quiero (aplicaré los mismos márgenes a los dos gráficas).
  • A continuación defino una variable data donde guardaré los datos a representar (usaré la misma fuente de datos para las dos gráficas).
  • Por último, defino dos contenedores svg y svg2 (uno para cada gráfica).

  • Calculo las dimensiones de ambos contenedores y se las aplico.

  • Inserto los contenedores para los ejes verticales y horizontales. Consigo los márgenes deseados «moviendo» sus coordenadas en función de los márgenes que deseo. Es interesante destacar que para posicionar los ejes horizontales en la parte inferior es necesario desplazarlos «al final» del elemento svg (height - margins.bottom).

  • Inserto un elemento path que sera el que usaré a su vez parar contener la gráfica (por ahora, me limito a insertarlos sin hacer todavía ningún tipo de procesamiento).

Ahora comienza la magia. Defino las funciones de mapeo:

  • Para mapear las coordenadas x sólo necesito un mapeador xRange para ambas gráficas (se mapean del mismo modo).
  • Pero para mapear las coordenadas y necesito dos mapeadores yRange y yRange2 (un mapeador para cada gráfica).

Si os fijáis, los mapeadores tienen configurados el rango de dominio de nuestros valores (en nuestro caso, de 0 a 100) y el rango de coordenadas dondes hay que mapearlos:

  • Para el primer grafico el valor 0 se mapeará en el punto más alejado (o sea, abajo del todo).
  • Mientras que para el segundo gráfico el valor 0 se representará en el punto margins.top que es el más cercano al origen de coordenadas (o sea, arriba del todo).

  • Defino los ejes verticales y horizontales de mis dos gráficas (ojo: los defino pero todavía no los dibujo).
  • Si os fijáis, veréis que como parte de la definición cada eje se le configura el «escalador» que va a usar.
  • Por este motivo, ambas gráficas compartirán la misma definición para sus ejes x (xAxis) mientras que cada gráfica tendrá su propia definición del eje y (yAxis y yAxis2).

  • La última de las definiciones que necesitamos corresponden a las funciones que dibujarán las gráficas.
  • La primera gráfica usará los mapeadores xRange y yRange.
  • La segunda gráfica usará los mapeadores xRange y yRange2.

  • Una vez definidos todos los elementos que van a componer las dos gráficas,defino la función redrawData que será la responsable de dibujarlas.
  • Lo primero que hace es personalizar nuestro mapeador xRange para que abarque los valores mínimo y máximo del conjunto de valores a representar (es el mismo para ambas gráficas).
  • A continuación, dibuja los ejes horizontal y vertical de la primera gráfica y de la segunda gráfica. ¿Recordáis los contenedores que definimos al principio del código (un poco desplazados para respetar los márgenes) y los ejes que definimos a continuación? Pues ahora llega por fin el momento de dibujarlos (dibujo el eje dentro del contenedor).
  • Por último, dibuja los valores proporionados por valueFunc y valueFunc en sus respectivos contenedores graphLine y graphLine2 respectivamente.

  • Por fin, defino mi fuente de datos y la dibujo por primera vez.
  • Me quedo «escuchando» el evento que anuncia la llegada de nuevos valores para volver a dibujar la gráficas.

¿Conclusiones?

Honestamente, repasando los posts que he realizado sobre d3, me doy cuenta que algunos de los pasos y de las técnicas no las he explicado con la extensión y detalle que se merecen. Espero que este pequeño post ayude a todos los que quieran aprender d3 (lo mismo que otros posts me ayudaron a mí cuando inicié esta aventura).