Practicando con D3.js y datos del IBEX35 – Parte I

Ultimamente, he estado estudiando las librerías javascript D3.js. La verdad, es que me ha gustado bastante su planteamiento. Porque, en lugar de ser una librería orientada a facilitar la publicación de gráficas clásicas con más o menos opciones de configuración (que es lo que yo presuponía), su diseñador ha decidido crear una serie de recursos muy básicos (pero al mismo tiempo, muy potentes) de propósito general que facilitan la tarea de crear visualizaciones de cualquier tipo asociadas a series de datos de diversa naturaleza.

Leamos la descripción que figura en su propia página web:

«D3 allows you to bind arbitrary data to a Document Object Model (DOM), and then apply data-driven transformations to the document. For example, you can use D3 to generate an HTML table from an array of numbers. Or, use the same data to create an interactive SVG bar chart with smooth transitions and interaction.»

«D3 te permite asociar un conjunto arbitrario de datos a un DOM, y aplicar entonces al documento transformaciones orientadas a los datos. Por ejemplo, puedes usar D3 para generar una tabla HTML a partir de un array de números. O puedes usar esos mismos datos para crear una gráfica SVG interactiva con transiciones y que responda a interacciones.»

Nos lo dice claramente: podemos usar D3 tanto para generar tablas HTML o para generar gráficas SVG. Eso sí, donde realmente se aprecian las posibilidades de D3 será cuando generemos gráficas SVG. Porque, aparte de las funciones primitivas de propósito general, tiene funciones específicamente orientadas a generar visualizaciones SVG.

Las posibilidades que ofrece son, por lo tanto, inmensas. Porque al no limitarse a crear las gráficas tradicionales (barras verticales, barras horizontales, líneas, tartas, etc) el límite lo pones tú con tu imaginación y tu capacidad para dominar estas primitivas (aunque también te hará falta dominar los elementos SVG).

Los datos del IBEX35

Para comenzar a practicar con esta librería y sus posibilidades, he decidido jugar con unas fuentes de datos que siempre me han llamado la atención: los índices bursátiles. Sin llegar en absoluto a dedicarme al análisis bursátil de tipo técnico, siempre me ha fascinado ver cómo las gráficas bursátiles evolucionan con el tiempo. Así que me ha parecido que ésta es una buena oportunidad para jugar, como digo, con esta fuente de datos.

Entre todas las posibles fuentes de datos bursátiles, he decidido centrarme en el IBEX35. El motivo es que es un índice compuesto y me resultaba mucho más adecuado para los estudios de evolución que me gustaría realizar que si hubiera escogido un índice de una empresa individual. Para acceder a sus datos, he usado los datos que he recogido de Quandl. Me hubiera gustado encontrar alguna fuente de datos online (con una API a la que pedir estos datos) pero como mi prioridad era practicar con D3, he preferido descargar los datos del IBEX35 desde Quandl y recopilarlos en un fichero CSV que usaré en mis prácticas.

Práctica 1: Volcar todo el histórico de datos en una gráfica lineal

Vamos a ver primero el resultado final:

Podéis abrir este mismo resultado en una ventana independiente aquí. Si lo investigáis con cualquier herramienta de inspección del DOM como Firebug (o cualquiera de las herramientas que ya vienen integradas con la nueva generación de navegadores) veréis que lo que he generado usando D3 es un objeto SVG y que podéis ver su estructura y los distintos elementos SVG que lo componen.

El código HTML

El código HTML asociado no lo he complicado mucho: lo he generado usando una plantilla de HTML5 y Twitter Bootstrap de yeoman. Aparte, tiene asociadas las librerías javascript jQuery y D3.

El único elemento importante del documento es éste:

O sea, defino un div con id ibex35 que será donde mi código javascript insertará mi gráfica SVG.

La fuente de datos

Como he adelantado un poco antes, he preparado un fichero en formato CSV donde he recopilado el histórico de datos. Este fichero, tiene la siguiente estructura:

Podéis comprobar que su estructura es muy simple:

  • La primera línea, indica los nombres de los dos datos: «Date» y «High».
  • Una línea por movimiento.
  • Cada línea tiene dos datos, separados por comas: la fecha y el valor.

(El valor tiene como nombre «High» porque de todos los posibles valores asociados al día (valor de apertura, valor de cierre, mínimo del día, máximo del día), me he concentrado en el valor máximo que alcanzó en el día).

El código javascript

El código javascript asociado es el siguiente:

Vamos a analizar este código, poco a poco:

Carga del código javascript

  • Intentemos mantener las buenas costumbres: activo el modo strict de javascript y encapsulo todo mi código dentro de una IIFE (Immediately-Invoked Function Expression, prefiero no traducirlo). A esta IIFE, le paso como parámetro una referencia al objecto window.d3.
  • Aprovecho el evento ready de jQuery para cargar, por fin, todo mi código.

Definición de variables globales

Declaro una serie de variables que usaré en el resto del código:

  • data, para guardar los datos a mostrar en la gráfica, una vez que los lea de su fuente.
  • width, para la anchura de mi gráfica. La calculo a partir de la anchura del div contenedor (que a su vez, está configurado por css para tener una anchura del 95% del ancho posible).
  • height, para la altura de mi gráfica.
  • margins es un objeto donde guardo los márgenes internos dentro de la gráfica. Estos márgenes internos, están reservados para mostrar información complementaria (ejes, títulos, leyendas, etc).

Lectura de la fuente de datos

Al final del todo el código, leo la fuente de datos:

  • Uso la función d3.csv para leer los datos. Esta función, está ya preparada para parsear correctamente un fichero formateado como CSV. Devuelve un Array donde cada elemento es un objeto que tiene una propiedad por cada columna definida dentro del fichero. Como el fichero tiene configurados que las dos columnas se llaman «Date» y «High», las dos propiedades tienen los mismos nombres.
  • Cuando termine la lectura de los datos, se invocará al método .get(). Dentro de este método, asigno los datos leídos (rows) a la variable data y llamo a mi función redrawData().
  • El punto más sutil es el método .row(). Este método será invocado para cada fila de datos leída y permite hacer un procesamiento de cada línea. Los datos son importados siempre como tipo String. En este caso, como sé que la propiedad Date tiene una fecha, lo convierto en un objeto Date. Además, como sé que la propiedad High tiene un número, lo convierto a número. De este modo, mi función redrawData() encontrará un array con datos perfectamente procesados.

Dibujo la gráfica

Por último, la función redrawData() que será la encargada de dibujar la gráfica:

Para aquellos que estamos acostumbrados a jQuery, la lectura de esta primera parte es casi inmediata:

  • Selecciona el div con id ibex35 (el que definimos en nuestro documento html para contener la gráfica).
  • Le añade a este div un elemento svg.
  • A este elemento svg, le configura sus atributos width y height con los valores de las variables definidas previamente.
  • Por último, guardamos una referencia a este elemento svg recién añadido dentro de la variable svg para poder manipularlo un poco más adelante.

Efectivamente, D3 nos permite seleccionar elementos del DOM usando el método selectAll (tiene otro más, select que es ligeramente distinto). selectAll espera como argumento cualquier selector CSS válido.

También podemos añadir nuevos elementos usando el método append. En este caso, hemos añadido un elemento svg pero podíamos haber añadido cualquier elemento html válido (un div, un table, etc).

Además, lo mismo que ocurre con jQuery, los métodos D3 son encadenables.

Defino las escalas horizontal y vertical

¿Para qué sirven estas dos escalas xRange y yRange? Veamos:

  • Tenemos un objeto svg al que tendremos que añadir todos los elementos SVG que componen nuestra gráfica. Esto, supone posicionar estos elementos dentro del svg para que su posición represente visualmente la información que necesitamos mostrar.
  • Así, si tenemos un eje horizontal que representa la evolución temporal y tenemos que pintar un circulo para representar una fecha determinada dentro de este eje horizontal, tendremos que calcular la posición horizontal donde tenemos que dibujarlo para que refleje fielmente la posición temporal que representa. Cada fecha, tendrá asignada una posición más o menos hacia la derecha en función de que la fecha sea más o menos moderna.
  • No es demasiado problemático escribir una función para efectuar este mapeo / escalado entre el valor real y su posición dentro del objeto svg. Pero no es necesario programar esta función, porque D3 ya nos proporciona una serie de funciones para realizarlo. Digo que son varias funciones, porque nos podemos encontrar varios tipos de ejes y por lo tanto varios tipos de mapeos.

Como he explicado, nuestro eje horizontal representa una escala temporal. Para representar escalas temporales, usaremos d3.time.scale:

  • Creo una escala temporal usando d3.time.scale.
  • Uso el método .range para asignar los valores que abarcarán las posiciones extremas dentro del gráfico. En nuestro caso, son las coordenadas izquierda y derecha. Además, fijaros que dichas posiciones no abarcan el 100% de la anchura del objeto svg (desde 0 hasta width) sino que abarca un poco menos, desde margins.left hasta width - margins.right para dejar libres los márgenes izquierdo y derecho.
  • Por último, el método .domain configura qué valores se mapean con estas posiciones. En nuestro caso, serán las fechas mínima y máxima que encontremos dentro del fichero csv que leamos.
  • Para calcular las fechas mínima y máxima, usamos las funciones d3.min y d3.max que nos permiten recorrer nuestro array de datos data e indicamos que queremos buscar los mínimos y máximos de la propiedad date.

El eje vertical, lo usaré para representar los valores. Es código es muy parecido … pero con una complejidad adicional:

  • Como son valores numéricos contínuos, usaré d3.scale.linear
  • Para configurar las posiciones verticales mínima y máxima, usaré de nuevo el método .range.

Pero para definir estos valores mínimo y máximo, tenemos un problema que no tuvimos al definir el eje horizontal: para posicionar elementos dentro de nuestro objeto svg, su origen de coordenadas está en el extremo superior izquierda … mientras que el origen de coordenadas para nuestra gráfica queremos que esté en el extremo inferior izquierda ¿Qué consecuencias tiene esta diferencia?:

  • En el eje horizontal el valor mínimo (la fecha más antigua) corresponde con la posición más pequeña (margins.left) y el valor máximo corresponde con la posición más grande (width - margins.right).
  • En el eje vertical, por el contrario, el valor mínimo querremos posicionarlo abajo del todo (que corresponde con la posición más alta) mientras que el valor máximo lo querremos posicionar en la parte superior (o sea, la posición más baja).
  • Por eso, al definir el .range de nuestro eje vertical, indicamos que el extremo inferior es el mayor valor (height - margins.bottom, en lugar de height para preservar un margen inferior) mientras que el extremo superior es el valor más pequeño (margins.top en lugar de 0 para preservar un margen superior).

Defino los ejes vertical y horizontal

La función d3.svg.axis sirve para definir un eje. Creo que es de compresión directa:

  • Usa el método .scale para configurar qué valores abarca. A este método, le pasa las variables xRange e yRange que hemos definido previamente.
  • El método .tickSize sirve para indicar cuántas marcas queremos poner en el eje. Ojo: el eje es inteligente. ¿Qué quiere decir esto? Que usará el valor que le pasemos a .tickSize como referencia pero a la hora de dibujarlo podrá añadir / quitar marcas en función de los valores a representar.
  • El método .orient sirve para indicar en qué lado (arriba, abajo, izquierda, derecha) vamos a dibujar el eje. Así, las marcas las dibujará en la parte exterior.

IMPORTANTE: d3.svg.axis define el eje pero no lo dibuja (lo dibujaremos un poco más adelante, usando las referencias xAxis e yAxis).

Defino la gráfica

Tal como explicaba antes, D3 proporciona una serie de funciones primitivas. Podía haber usado estas funciones primitivas para dibujar mi gráfica, línea a línea. Pero también explicaba que D3 proporciona funciones específicas para manejar elementos SVG. Como esta función d3.svg.line que permite definir una gráfica linea de una sola tacada:

  • La gráfica, estará definida por una serie de puntos con coordenadas (x,y) enlazados por líneas.
  • Las posiciones x la definimos mediante una función anónima. ¿Qué recibe esta función? un dato d. ¿Qué devuelve esta función? la posición horizontal asociada a este dato d. En nuestro caso, usamos la variable xRange que tiene configurado el eje horizontal y le paso como parámetro la propiedad date del dato d.
  • Para las posiciones y le paso otra función anónima similar a la anterior. Excepto que la función que calcular el mapeo es la variable yRange a la que le paso el valor d.high a mapear en la gráfica.
  • Por último, uso .interpolate para definir que quiero conectar los puntos mediante líneas.

De nuevo, lo mismo que el paso anterior no he dibujado nada. Sólamente he definido cómo quiero que sea mi gráfica. Si os fijáis, he definido los mapeos y todo está preparado para cuando le pase a esta funcion valueFunc los datos a mapear (un poco más adelante veremos cómo).

Dibujo los ejes horizontal y vertical

Por fin, dibujo los ejes horizontal y vertical que he definido previamente en las variables xAxis e yAxis:

  • Añado un elemento g al que le asigno las clases x axis
  • A este elemento g le someto un cambio de coordenadas a la posición (0, height - margins.bottom) (o sea, la esquina inferior izquierda)
  • Con este elemento g correctamente, posicionado, dibujo dentro de él mi eje horizontal llamando .call(xAxis).
  • Para el eje vertical, el procedimiento es similar pero lo posiciono en (margins.left, 0) (o sea, la esquina superior izquierda)

Dibujo la gráfica

Por último, dibujo la gráfica:

  • Añado un objeto path con una serie de atributos stroke, stroke-width y fill.
  • Uso .attr('d', valueFunc(data)) para pasarle a este objeto path la función ValueFunc que definí previamente. A esta función, le paso como parámeto la variable data que tiene los datos a dibujar … y D3 se encarga de todo lo demás :).

Conclusiones

Tal como decía al principio, me ha gustado mucho el diseño de D3. Su organización en funciones básicas orientadas a facilitar el mapeo entre datos y objetos DOM es impresionante. Para este ejemplo concreto, hemos usado la función d3.svg.line que nos ha facilitado la tarea de dibujar la gráfica. Pero también podríamos haber dibujado nuestra gráfica línea a linea si lo hubiéramos necesitado. Los tipos de gráficas que podemos obtener no tienen límites más que nuestra imaginación y el dominio de las primitivas. En futuras entradas, intentaré ir añadiendo más funciones primitivas de D3 conforme vaya retocando esta primera gráfica añadiéndole más componentes.

En cuanto a la curva de aprendizaje de D3, personalmente me he sentido muy cómodo. Pero estamos hablando de que tengo bastante rodaje tanto con javascript y jQuery, lo que facilita mucho la vida a la hora de manejar D3. Si no hubiera tenido esta experiencia, se hubiera hecho mucho más cuesta arriba.

No sé si este artículo servirá a mucha gente, porque no me he dedicado a explicar detalladamente las funciones que he usado. Me he limitado a usarlas lo más claramente posible y a explicarlas por encima. Si alguien tiene dudas o comentarios, son bienvenidos. Y si a alguien le ha resultado interesante, me alegro.

En próximas entradas, como digo, iré retocando este ejemplo básico. De este modo espero que el lector pueda estudiar cómo el código evoluciona y cómo uso los distintos elementos de D3 que he ido incorporando a mi arsenal (¡realmente, no he hecho más que empezar!)

2 comentarios a “Practicando con D3.js y datos del IBEX35 – Parte I

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

*
*
Sitio Web