Crear una figura con D3

D3.js es una librería de JavaScript que permite hacer representaciones gráficas dinámicas en HTML. Conecta dos mundos: HTML y datos. Se recomienda leer la introducción con un ejemplo de representación de texto en D3 publicada en nuestra web, así como hacer un repaso a lo más básico de HTML y JavaScript. Si ya lo has estudiado o lo dominas porque no puedes evitar ser un geek, continúa leyendo. Ahora vamos a crear una figura con D3, viendo los detalles problemáticos y cómo pueden solucionarse.

Crear una FIGURA con D3

Cuando vamos a crear una figura con D3, lo más importante es saber con qué formato estaremos trabajando. Este formato es un formato vectorial, más específicamente D3 trabaja con formato SVG (Scalable Vector Graphics). Estasque son figuras generadas vectorialmente que además ocupan muy poco espacio y nunca pierden resolución al ampliar.

Lo primero que tenemos que hacer es crear el archivo HTML con su estructura básica. A esta estructura básica hay que hacer una llamada a la librería de D3 y aun script en JavaScript que creamos nosotros en la misma carpeta.

<html>
	<head>
		<script src="https://d3js.org/d3.v5.min.js"></script>
		<script src="script.js"></script>
	</head>
	<body>
	</body>
</html>

En esta entrada vamos a seguir con los datos de los partidos políticos de los que ya hablamos en la introducción a D3.

Creamos un archivo script.js donde cargamos los datos de partidos políticos que están en JSON. Cuando creamos el contenedor, tenemos que generar un objeto SVG en el cuerpo del código HTML. El contenedor padre sería el body, mientras que el contenedir hijo sería svg, que llamaríamos con .append(). La variable que generamos con los contenedores la llamamos figuraSVG. El código hasta este paso, incluyendo la carga de los datos JSON y la creación de los contenedores, sería el siguiente:

d3.json("http://output.jsbin.com/lixujex/1.js").then (function (partidos) {
	
	//Confirmamos que los datos están cargados
	console.log("Los datos se han cargado satisfactoriamente")
	
	// Generamos el contenedor
	var elementoSVG = d3.select("body")
		.append("svg")

Seguidamente, lo que hacemos es añadir la zona y dimensiones del SVG que queremos crear. Esto lo hacemos con sucesivas funciones .attr(), especificando el ancho (width), altura (height). Si queremos la figura con 500px de ancho y alto, el código sería el siguiente:

var figuraSVG = d3.select("body")
	.append("svg")
	.attr("width", 500)
	.attr("height", 500)

Como queremos dibujar círculos, creamos una selección vacía con esta característica, realizando el join justo después. Acto seguido, le indicamos que añada los círculos.

var figuraSVG = d3.select("body")
	.append("svg")
	.attr("width", 500)
	.attr("height", 500)
	.selectAll("circle") // selección vacía
	.data(partidos)
	.enter()
	.append("circle") 

Cuando vemos el Inspector de nuestro archivo index.html, vemos que se han generado 44 círculos en el cuerpo del HTML, pero que de momento no pintan nada. Ha identificado correctamente que tenemos 44 partidos políticos en nuestro conjunto de datos, integrados en una figura SVG de 500px de anchura y altura.

Ahora lo que nos queda es dibujar los círculos. Vamos a trazarlos, y para ello necesitamos especificar el radio de los mismos. Lo conseguimos añadiendo otro .attr(). Dentro de él especificamos el radio de los círculos en 100 con la siguiente línea .attr("r", 100).

var figuraSVG = d3.select("body")
	.append("svg")
	.attr("width", 500)
	.attr("height", 500)
	.selectAll("circle") // selección vacía
	.data(partidos)
	.enter()
	.append("circle") 
	.attr("r", 100)

Al abrir el archivo HTML, vemos que nos ha dibujado un cuarto de círculo en la esquina superior izquierda.

Realmente, existen 44 círculos superpuestos de 100px de radio. Están superpuestos porque no se han especificado las coordenadas XY de cada uno de ellos. Y sabiendo que el (0,0) se sitúa en la esquina superior izquierda de la imagen, y que ese (0,0) es justo el centro del círculo, podemos entender por qué aparece solo un cuarto.

Tenemos que especificar la posición X e Y de cada uno de ellos. Esos atributos son cx y cy, respectivamente. Vamos a ver qué variables vamos a representar en cada coordenadas.

Coordenada X: mediaAutoubicacion

En la coordenada X (cx) vamos a representar la variable mediaAutoubicacion, que representa un valor por partido político para ver si está más a la izquierda o más a la derecha. El intervalo de esta variable se sitúa entre de 0 a 10, de más a la izquierda o más hacia la derecha. Como los valores están entre 0 a 10, al representarlos en D3 aparecerán todos solapados en apenas 10 píxeles. Vamos a verlo añadiendo otro atributo (.attr("cx", d=>d.mediaAutoubicacion)

El cambio de posición es muy sutil, ya que están los 44 círculos en una escala de 10 píxeles. Por lo tanto, tenemos que emplear las escalas con dominios y rangos para relativizarlo. Como nuestra dimensión de SVG es 500px, este va a ser el rango.

Generamos una escala lineal con dominio de 1 a 10 y rango de 0 a 500. La vamos a llamar escX. Además, vamos a reducir el radio a 25 para que se visualicen mejor los círculos.

// Generamos la escala de X
var escX = d3.scaleLinear()
	.domain([1, 10])
	.range(["0", "500"])

// Generamos el contenedor
var figuraSVG = d3.select("body")
	.append("svg")
	.attr("width", 500)
	.attr("height", 500)
	.selectAll("circle") // selección vacía
	.data(partidos)
	.enter()
	.append("circle")
	.attr("r", 25)
	.attr("cx", d=>escX(d.mediaAutoubicacion))

Coordenada Y: votantes

De un modo análogo a la coordenada X, vamos a representar en la escala Y el número de votantes. Los valores de votantes oscilaban entre 0 y 3000, así que construimos la escala de Y (escY) para relativizar los valores en ese intervalo. El código combinando la coordenada X e Y sería el siguiente:

// Generamos la escala de X
var escX = d3.scaleLinear()
	.domain([1, 10])
	.range(["0", "500"])
	
// Generamos la escala de Y
var escY = d3.scaleLinear()
	.domain([0, 3000])
	.range(["0", "500"])

// Generamos el contenedor
var figuraSVG = d3.select("body")
	.append("svg")
	.attr("width", 500)
	.attr("height", 500)
	.selectAll("circle") // selección vacía
	.data(partidos)
	.enter()
	.append("circle")
	.attr("r", 25)
	.attr("cx", d=>escX(d.mediaAutoubicacion))
	.attr("cy", d=>escY(d.votantes))

Coordenada X: mediaAutoubicacion

Como el 0,0 está en la esquina superior izquierda, vamos a invertir el orden del eje Y. Esto lo podemos realizar cambiando el orden de los valores del rango de la escala Y (escY).

var escY = d3.scaleLinear()
	.domain([0, 3000])
	.range(["500", "0"])

De este modo, nuestra esquina 0,0 estará en la parte inferior izquierda de nuestro SVG de 500px de lado.

Si queremos que seleccione la escala el dominio automáticamente, utilizamos extent del siguiente modo. Así no tenemos que preocuparnos por saber cuáles son los límites mínimo y máximo, ya que esta función lo hará por nosotros. Por lo tanto, permite dinamizar el código y ser más flexible.

var escY = d3.scaleLinear()
	.domain(d3.extent(partidos, d=>d.votantes))
	.range(["500", "0"])

Cambiar los colores de los círculos con una escala

Para ajustar el color de los círculos según el grado político (mediaAutoubicacion), tenemos que generar una escala de color. La escala se va a formar de un modo similar a las que hemos creado anteriormente, aunque indicando en el rango los colores. Vamos a seleccionar que vaya desde el rojo hasta el azul, y llamaremos a la escala como escCol.

// Generamos la escala de color
var escCol = d3.scaleLinear()
	.domain([1, 10])
	.range(["red", "blue"])

Por último, añadimos otro .attr() al final con "fill" y aplicando la escala escCol.

var figuraSVG = d3.select("body")
		.append("svg")
		.attr("width", 500)
		.attr("height", 500)
		.selectAll("circle") // selección vacía
		.data(partidos)
		.enter()
		.append("circle")
		.attr("r", 25)
		.attr("cx", d=>escX(d.mediaAutoubicacion))
		.attr("cy", d=>escY(d.votantes))
		.attr("fill", d => escCol(d.mediaAutoubicacion))

Como podemos ver, se ha generado un gradiente de color de rojo (a la izquierda) a azul (en la derecha).

Modificar márgenes para ver los círculos completos

Uno de los problemas de la figura anterior es que los círculos que están en los márgenes están cortados por la mitad. Como comentamos inicialmente, las coordenadas para posicionar los círculos están en el centro de los mismos.

Tenemos que jugar con los rangos. Puesto que hemos marcado en el eje X e Y un rango de 0 a 500, y teniendo en cuenta que las dimensiones del SVG son de 500px, podemos solucionarlo bajando el rango de nuestras escalas. Vamos a modificar escX y escY para poner rangos entre 50 y 450.

// Generamos la escala de X
var escX = d3.scaleLinear()
	.domain([1, 10])
	.range(["50", "450"])
	
// Generamos la escala de Y
var escY = d3.scaleLinear()
	.domain(d3.extent(partidos, d=>d.votantes))
	.range(["450", "50"])
Este es el resultado final de crear una figura con D3 mediante el uso de los datos de partidos políticos.

Pero este modo de proceder supone un problema serio, ya que tenemos que evaluar manualmente los márgenes. Necesitamos que sea algo dinámico y manual.

Para este fin podemos crear una nueva variable en script.js, que llamaremos margen. Esta variable va a contener los 4 márgenes de la figura. Además creamos dos variables con las dimensiones del SVG final que queremos.

// Generamos márgenes
var margen = {
	arriba: 45,
	abajo: 40,
	izq: 50,
	der: 55
}

// Definimos dos dimensiones del SVG
var altura = 400
var anchura = 600

Un último paso antes de seguir es de cambiar los valores de width y height del contenedor que se hizo al principio, para que tenga en cuenta los nuevos valores introducidos.

	// Generamos el contenedor
	var figuraSVG = d3.select("body")
		.append("svg")
		.attr("width", anchura)
		.attr("height", altura)

Actualizamos los rangos de las escalas escX y escY en base a las variables creadas.

// Generamos la escala de X
var escX = d3.scaleLinear()
	.domain([1, 10])
	.range([0 + margen.izq, anchura - margen.der])
	
// Generamos la escala de Y
var escY = d3.scaleLinear()
	.domain(d3.extent(partidos, d=>d.votantes))
	.range([altura - margen.abajo, 0 + margen.arriba])

Se obtiene finalmente este resultado:

De este modo hacemos que los márgenes de la figura se ajusten automáticamente al margen de las dimensiones del SVG y los mismos márgenes de los rangos.

Archivo script.js final para crear una figura con D3

A continuación mostramos todo el código generado en el archivo script.js de esta entrada para poder crear una figura con D3.

d3.json("http://output.jsbin.com/lixujex/1.js").then (function (partidos) {
	
	//Confirmamos que los datos están cargados
	console.log("Los datos se han cargado satisfactoriamente")
	
	// Generamos márgenes
	var margen = {
		arriba: 45,
		abajo: 40,
		izq: 50,
		der: 75
	}
	
	// Definimos dos domensiones del SVG
	var altura = 400
	var anchura = 600
	
	// Generamos la escala de X
	var escX = d3.scaleLinear()
		.domain([1, 10])
		.range([0 + margen.izq, anchura - margen.der])
		
	// Generamos la escala de Y
	var escY = d3.scaleLinear()
		.domain(d3.extent(partidos, d=>d.votantes))
		.range([altura - margen.abajo, 0 + margen.arriba])
		
	// Generamos la escala de color
	var escCol = d3.scaleLinear()
		.domain([1, 10])
		.range(["red", "blue"])
	
	// Generamos el contenedor
	var figuraSVG = d3.select("body")
		.append("svg")
		.attr("width", anchura)
		.attr("height", altura)
		.selectAll("circle") // selección vacía
		.data(partidos)
		.enter()
		.append("circle")
		.attr("r", 25)
		.attr("cx", d=>escX(d.mediaAutoubicacion))
		.attr("cy", d=>escY(d.votantes))
		.attr("fill", d => escCol(d.mediaAutoubicacion))
})

Deja un comentario