Experimentación con D3JS. En este primer laboratorio se elige un simple gráfico de barras para la toma de contacto, cómo beber de una fuente de datos externa, y mostrar un tooltip con el detalle de los campos retornados.
Índice de contenidos
- 1. Gráfico de barras con D3JS
- 2. Un primer acercamiento a SVG
- 3. El mismo ejemplo con D3JS
- 4. Fuente externa de datos
- 5. Formateando números con D3JS
- 6. Una pausa para reflexionar
- 7. Añadiendo los ejes y magnitudes
- 8. Añadir un tooltip que muestre el detalle
- 9. Conclusiones
- 10. Enlaces y referencias
1. Gráfico de barras con D3JS
Este artículo es el primero de una serie de laboratorios, para aprender y probar D3JS.
D3JS es una poderosa librería que mediante estándares web nos permite representar gráficos estadísticos complejos. Se apoya en el estándar de gráficos vectoriales SVG, de CSS3 para los estilos sobre los gráficos y JavaScript para tratar con los datos.
La representación de datos numéricos en un modelo que se preste a una rápida interpretación visual es un arte. A veces conviene usar unos modelos en lugar de otros. Hay muchísimos: gráficos de barras, curvas de nivel, grafos de densidad, redes con peso en los nodos, mapas de calor, distribuciones sobre mapas geolocalizados, nubes de dispersión, etc… ¿Cuál elegir? El que represente más fielmente la información y se preste a una interpretación sencilla. De nada nos sirven gráficos complejos que nos cuesta interpretar. Y ahí prima el sentido común. No es lo mismo tratar con datos de personas por rangos de edades para ambos sexos, donde a lo mejor es conveniente representarlo en forma de pirámide poblacional, que con casos de gripe por cada mil habitantes en España, donde una proyección sobre el mapa de la península parece inevitable.
D3JS nos permite hacer todo eso, pero la curva de aprendizaje no es elemental y requiere de una aproximación a esta tecnología mediante la propia experimentación.
En este primer ejercicio, me fijo la meta de dibujar un simple gráfico de barras en base a una petición REST que me devuelva los sueldos medios en Europa durante el 2014.
2. Un primer acercamiento a SVG
Dados los datos <[135, 100, 150, 125, 225, 175]> se quiere dibujar un gráfico de barras. Para ello, haremos uso de la etiqueta <svg> a la que hay que indicarle un ancho y un alto, y dibujaremos seis rectángulos con la altura indicada en los datos.
<svg width="315" height="410"> <rect x="10" width="45" height="135" /> <rect x="60" width="45" height="100" /> <rect x="110" width="45" height="150" /> <rect x="160" width="45" height="125" /> <rect x="210" width="45" height="225" /> <rect x="260" width="45" height="175" /> </svg>
Esto tiene el indeseable efecto de desplegar las barras hacia abajo, en lugar de crecer hacia arriba desde la línea base (eje X). ¿Esto por qué pasa? Todo lo que está dentro de la etiqueta SVG se posiciona de forma relativa, siendo el origen de coordenadas la esquina superior izquierda. De esta forma, igual que para cada columna nos posicionamos en su coordenada horizontal (“x”), a partir de la cual se suma su ancho, lo mismo hay que hacer con la coordenada vertical (“y”), a partir de la cual se suma su altura.
<style> svg { border: 1px solid #000000; } rect { stroke: SteelBlue; stroke-width: 2; fill: LightSteelBlue; } rect:hover { fill: SteelBlue; } </style> <svg width="315" height="410"> <rect x="10" width="45" y="265" height="135" /> <rect x="60" width="45" y="300" height="100" /> <rect x="110" width="45" y="250" height="150" /> <rect x="160" width="45" y="275" height="125" /> <rect x="210" width="45" y="175" height="225" /> <rect x="260" width="45" y="225" height="175" /> </svg>
Si queremos que todas las barras acaben en un supuesto eje horizontal, tenemos que hacerlo manualmente, de forma que la “y” y el “height” sumen lo mismo para cada barra, en el ejemplo que nos ocupa “400”. Y en la etiqueta svg se ha puesto que la altura es 410. Esos 10 píxeles de más es el margen que se ve en la imágen.
Para verlo mejor se ha añadido un borde de 1 píxel al estilo de la etiqueta svg. Otra cosa que observamos es que para dar estilo mediante CSS a los rectángulos no se usa background ni border, si no fill y stroke.
En este ejemplo, el gráfico respira mucho por arriba, pero se podía haber limitado la altura del svg al máximo de los datos más un margen. A todos estos cálculos nos ayuda D3JS.
3. El mismo ejemplo con D3JS
Se ve, que para seis columnas el código queda sencillo, pero que si son muchas más, hacer los cálculos es un engorro, además de que también nos puede interesar que los datos se carguen dinámicamente y sean cambiantes.
Vamos a ver cómo sería este ejemplo con D3JS:
<svg width="315" height="245"></svg> <script src="http://d3js.org/d3.v3.min.js"></script> <script> var datos = [135, 100, 150, 125, 225, 175]; var config = { columnWidth: 45, columnGap: 5, margin: 10, height: 235 }; d3.select("svg") .selectAll("rect") .data(datos) .enter().append("rect") .attr("width", config.columnWidth) .attr("x", function(d,i) { return config.margin + i * (config.columnWidth + config.columnGap) }) .attr("y", function(d,i) { return config.height - d }) .attr("height", function(d,i) { return d }); </script>
Este código obtiene el mismo resultado que el ejemplo anterior. Vemos que si en lugar de 6 columnas hubiera 600, el código no crecería. Y los cálculos no tenemos que realizarlos a mano cada vez.
Vamos a explicar qué hace el código.
Lo primero que se observa es que se importa la librería de d3js
<script src="http://d3js.org/d3.v3.min.js"></script>
Luego hacemos: d3.select(«svg») que es la forma que tenemos en D3JS para seleccionar un elemento del DOM, lo mismo que haríamos en jQuery con $(«svg») o con JavaScript clásico con document.getElementsByTagName(«svg»)[0] o con el nuevo selector que nos ofrece html5 document.querySelector(«svg»).
Con selectAll(«rect») seleccionamos todos los rectángulos que hubiera dentro de la etiqueta svg. Lo cierto es que hasta ahora no hay ninguno, pero si los hubiera, le aplicaría a cada uno lo que venga en el array datos.
Con enter().append(«rect») indicamos que si le faltan rectángulos para los elementos del array, que los cree. Y el resto son indicaciones de como calcular los distintos atributos de rect.
<rect x="160" width="45" y="275" height="125" />
La indentación escalonada no es casualidad: hay una convención, por la cual se indenta igual mientras no cambie los elementos del DOM seleccionados. Como con enter().append(«rect») añadimos elementos en el caso de que falten, se cambia la indentación.
4. Fuente externa de datos
Lo normal, es que los datos no sean estáticos, si no que cambien de forma dinámica y sea un servicio el que sirva dichos datos. De alguna forma tenemos que poder indicarle a D3JS ese DataProvider. Efectivamente, D3JS nos provee de una serie de métodos para poder cargar datos de recursos externos: ficheros de datos tabulados separados por comas (CSV), ficheros separados por tabuladores (TSV), XMLs, TXTs, un sin fin de formatos más, y por supuesto JSON, que es el que vamos a usar en nuestros ejemplos.
d3.json("salarioMedioEuropa.json", function(error, json) { if (error) { return console.warn(error); } renderData(json); }); function renderData(datos){ d3.select("svg") .selectAll("rect") .data(datos) .enter().append("rect") .attr("width", config.columnWidth) .attr("x", function(d,i) { return config.margin + i * (config.columnWidth + config.columnGap) }) .attr("y", function(d,i) { return config.height - d.salarioMedio }) .attr("height", function(d,i) { return d.salarioMedio }) .attr("data-nombre", function(d,i) { return d.nombre }) .attr("data-salarioMerdio", function(d,i) { return d.salarioMedio }) }
Ver ejemplo 3: SVG from external source
Esto debería mostrar los datos que vienen de la petición AJAX, que retorna los salarios medios en 40 países, pero vemos que el resultado no es el esperado.
Esto es porque efectivamente hemos mostrado los datos reales, pero la altura de las barras es en píxeles, y claro el salario medio anual de cualquier país es mayor que la altura que le hemos dado al gráfico SVG.
Podríamos dividir el salario medio de cada país para normalizar los datos, pero cabría preguntarse si D3JS tiene algún mecanismo para hacer esto.
Efectivamente lo tiene y se llama d3.scale, que pueden ser de muchos tipos: lineal, logarítmica, etc… Y se usan de la siguiente manera:
var SALARIO_MAX = d3.max(datos, function(d) { return +d.salarioMedio; }); var normalizeY = d3.scale.linear() .domain([0, SALARIO_MAX]) .range([0, config.height - 10]);
Por un lado se calcula cual es el valor máximo. Y por otro se devuelve la variable normalizeY que retorna una función, de forma que hace una regla de tres lineal proyectando cualquier valor situado entre 0 y el salario máximo a un valor situado en el rango indicado, que es entre 0 y la altura del SVG (menos 10 píxeles de margen).
[box style=»1″]
[icon icon=»exclamation»]NOTA:
Conviene fijarse en cómo se calcula el máximo, y en que el return de la función lleva el signo +, esto es por que al provenir de JSON, todos los campos vienen como string si no indicamos lo contrario, y el “8.000” es mayor que “40.000” al prevalecer el orden lexicográfico. Así que hay que forzar los tipos, para que sean de tipo numérico. Hay dos opciones: signo + por la izquierda, o multiplicar el valor por 1.
[/box]
Ahora, el gráfico queda como sigue.
Ver ejemplo 4: SVG with normalized data
5. Formateando números con D3JS
Ahora cada barra tiene la información siguiente:
<rect width="20" x="235" y="156.01693323314726" height="78.98306676685273" data-nombre="España" data-salarioMerdio="26162€"></rect>
Hemos guardado en cada barra el nombre del País y el salario medio del mismo, por si los necesitamos para hacer algo con la barra seleccionada, o para mostrar un tooltip, etc…
Se podría querer formatear el salarioMedio. Para eso nos provee de la función format que hace uso del minilenguaje de especificación de formatos de Python, el cual es ciertamente melindroso hasta que te lees el enlace anterior y lo entiendes todo.
var _format = d3.format("$,.2f");
Ahí le indicamos que queremos moneda, separador de miles, separador de decimales, con dos decimales y que es un número. Pero nos lo va a devolver en formato anglosajón. Para formatearlo en otro idioma tenemos que indicarle los parámetros del locale que queremos cambiar.
var ES_es = d3.locale ({ "decimal": ",", "thousands": ".", "grouping": [3], "currency": ["", " €"], "dateTime": "%a %b %e %X %Y", "date": "%d/%m/%Y", "time": "%H:%M:%S", "periods": ["AM", "PM"], "days": ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"], "shortDays": ["Dom", "Lun", "Mar", "Mi", "Jue", "Vie", "Sab"], "months": ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"], "shortMonths": ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"] }); var _format = ES_es.numberFormat("$,0f");
Ahora el formateo ya es el adecuado.
6. Una pausa para reflexionar
Hemos visto ya algunas cositas, pero no son ni la sombra de lo que puede hacer D3JS. Y sin embargo, cuando he empezado a hacer estos ejercicios me he sentido un poco incómodo, y es por cosas como esta:
var _format = ES_es.numberFormat("$,0f");
Aunque estamos declarando una variable, lo que se almacena en dicha variable es una función. En el momento en que se repara en este concepto, se ve claro como funciona D3JS. En casi todos los casos, cuando llamamos a un método de D3JS no nos devuelve un resultado, si no una función.
Así que realmente llamar a _format(26000) es lo mismo que llamar a (ES_es.numberFormat(«$,0f»))(26000)
Y lo mismo para otras variables que ya hemos visto, que en el fondo devuelven una función.
var normalizeY = d3.scale.linear() .domain([0, SALARIO_MAX]) .range([0, config.height - 10]);
En el momento que se entiende ésto, ya no se nos hace extraño, y al revés, se ve cómodo y de forma natural.
7. Añadiendo los ejes y magnitudes
Por un lado, estamos calculando a mano dónde cae cada columna en el ejeX y por otro habíamos creado una función normalizeY que proyecta nuestro rango sobre el ejeY. Vamos a ver si hay una forma de automatizar eso.
var config = { columnWidth: 20, columnHeight: 235, columnGap: 5, padding: 100}; function renderData(datos){ var NUM_COLUMNAS = datos.length; config.width = NUM_COLUMNAS * (config.columnWidth + config.columnGap) + (2 * config.padding); config.height = config.columnHeight + 2 * config.padding; var SALARIO_MAX = d3.max(datos, function(d) { return +d.salarioMedio; }); var x = d3.scale.ordinal() .rangeRoundBands([0, config.width - 2 * config.padding]) .domain(datos.map(function(d) { return d.nombre; })); var y = d3.scale.linear() .range([0, config.columnHeight]) .domain([0, SALARIO_MAX]); … }
De esta forma, la función x(“Suiza”) nos devolverá la coordenada en el ejeX donde se debe mostrar la columna correspondiente a Suiza
Ver ejemplo 5: SVG with normalized data
Ahora vamos con los ejes. Hay que mostrar las líneas del ejeX y del ejeY y alguna muesca que indique la magnitud de las cantidades que manejamos.
Para eso definimos una serie de variables con los ejes
var ejeX = d3.svg.axis().scale(x).orient("bottom");
var ejeY = d3.svg.axis().scale(y).orient("left");
Y ahora, como el gráfico SVG empieza a crecer mucho, lo almacenamos en una variable, para ir trabajando con él poco a poco.
var svg = d3.select("svg") .attr("width", config.width) .attr("height", config.height);
Y añadimos los ejes que hemos creado.
svg.append("g") .attr("class", "eje") .attr("transform", "translate(100,345)") .call(ejeX); svg.append("g") .attr("class", "eje") .attr("transform", "translate(90,100)") .call(ejeY);
Eso obtiene el siguiente resultado
Lo primero que se aprecia es que los nombres de los países se muestran en horizontal y se tapan unos a otros. Y lo segundo, es que el eje Y va de 0 a 70,000 empezando por arriba. Recordemos, que esto es porque la coordenada 0,0 está situada arriba a la izquierda.
Como el d3.svg.axis.scale() espera recibir una escala, tenemos que invertir la que tenemos. Lo más fácil es definirla justo al revés: de columnHeight a 0.
var rangeY = d3.scale.linear() .range([config.columnHeight, 0]) .domain([0, SALARIO_MAX]);
Y usar esta escala en el ejeY. Aprovechamos, y ya formateamos de paso las cantidades a mostrar en el ejeY.
var ejeY = d3.svg.axis() .scale(rangeY) .tickFormat(_format) .orient("left");
Para mostrar los países verticales, seleccionamos todos los elementos text una vez ya se han añadido al SVG y los rotamos 90 grados, lo posicionamos para que no tape la rayita y le ponemos text-anchor a start para que estén todos alineados al inicio, porque si no los alínea por defecto al medio.
svg.append("g") .attr("class", "eje") .attr("transform", "translate(100,345)") .call(ejeX); .selectAll("text") .attr("transform", "rotate(90)") .attr("x", "10") .attr("y", "-3") .style("text-anchor", "start");
Ver ejemplo 7: SVG with axis and formatted labels
8. Añadir un tooltip que muestre el detalle
A veces, si la cantidad de datos a mostrar es elevado, es conveniente filtrar la información relevante que se muestra en un primer vistazo, y ampliar la información con detalles pormenorizados. Para esto los tooltips son muy útiles.
D3JS tiene una librería que extiende la funcionalidad básica, y que dota a nuestras gráficas de esta posibilidad. La librería se llama d3-tip y es muy sencilla de utilizar.
Lo primero es descargarla e integrarla en nuestro proyecto.
Luego inicializamos el tooltip, se lo añadimos a nuestro gráfico SVG y lo llamamos. Posteriormente añadimos los controladores de eventos, para mostrarlo y ocultarlo.
var tooltip = d3.tip() .attr('class', 'tooltip') .offset([-10, 0]) .html(function(d) { return "<strong>" + d.nombre + "</strong><br>" + "salario medio: " + _format(+d.salarioMedio); }); var svg = d3.select("svg") .attr("width", config.width) .attr("height", config.height); svg.call(tooltip); [...] svg.selectAll("rect") .data(datos) .enter().append("rect") .attr("width", config.columnWidth) [...] .on('mouseover', tooltip.show) .on('mouseout', tooltip.hide) Y añadimos los correspondientes estilos CSS para adaptarlo a nuestro gráfico. .tooltip { font-family: Arial, helvetica, sans-serif; font-size: 10px; padding: 8px; background: rgba(0, 0, 0, 0.7); border: 1px solid #FFFFFF; box-shadow: rgba(0, 0, 0, 0.5) 1px 1px 4px; color: #FFFFFF; border-radius: 4px; }
Ver ejemplo 8: SVG with tooltip
9 . Conclusiones
La librería D3JS es excepcionalmente potente, pero requiere de un pequeño entrenamiento para poder hacer uso de ella. Hay que tener una visión conceptual del plano, de qué es lo que se está haciendo, y conocer un poquito de SVG. Lo demás viene rodado. Éste ha sido el ejercicio más elemental con el que nos podíamos poner, para que sirviera a modo de toma de contacto.
10 . Enlaces y referencias
- Puedes descargar este tutorial en PDF:
Autentia – 20160418 – Experimentando con D3JS. Gráfico de Barras - El código fuente de este tutorial en:
https://github.com/eContento/lab-d3js - D3JS API Reference
https://github.com/mbostock/d3/wiki/API-Reference - Web Oficial D3JS
https://d3js.org - Dando estilos a los SVGs con CSS
https://www.w3.org/TR/SVG/styling.html - Minilenguaje de especificación de formatos de Python
https://docs.python.org/release/3.1.3/library/string.html#formatspec
D3 es una excelente libreria, a mi me toco trabajar creando algunas de sus graficas con base a ejemplos y ya cuando habia terminado me dijeron:
muy bonitas graficas y los efectos estan padres, pero necesitamos que estas graficas se envien por correo a el directivo tal en formato PDF xD, por suerte me gusta Nodejs y descubri que podia correr D3 en el servidor, fue un rollo pero al final quedo «bien».
Dejo la historia completa aquí
http://www.ingenieroperales.com/2015/07/06/graficas-d3-con-nodejs/
Jesús,
Muchas gracias por tu aportación.
La verdad es que es una solución muy acertada.
Un saludo