Mejorando el gráfico de barras

Como hemos visto en el vídeo anterior, el gráfico es aún pobre. Deberíamos añadir los valores con el número de casos y los nombres de las provincias. Por otra parte, los colores no significan nada y podríamos usarlos para indicar que provincias están en la misma Comunidad Autónoma. Finalmente, añadiremos una sencilla transición para mostrar como funcionan y que el gráfico tenga animación.

Actualizamos el tamaño del gráfico

Como hemos dicho, no caben todas las provincias que existen en un gráfico pequeño. Asi pues, lo primero que haremos será hacer el gráfico un poco más grande y las barras un poco más estrechas:

import * as d3 from "d3";

interface ProvinciaData {
  code: string;
  name: string;
  ccaa: string;
  cases: number;
}
const data: ProvinciaData[] = require("./casos_provincia.json");

- const svgDimensions = { width: 1024, height: 768 };
+ const svgDimensions = { width: 1024, height: 1100 };
const margin = { left: 5, right: 5, top: 10, bottom: 10 };

...

svg
  .selectAll("rect")
  .data(data)
  .join("rect")
  .attr("x", 0)
- .attr("y", (d, i) => i * 30)
+ .attr("y", (d, i) => i * 20)
- .attr("height", 25)
+ .attr("height", 15)
  .attr("width", (d) => scale(d.cases))
  .attr("fill", (d, i) => d3.schemeSet3[i % d3.schemeSet3.length]);

Cambiar la escala de colores

La escala de color debería tener tantos colores como Comunidades Autónomas (más Ceuta y Melilla). Lo primero es obtener la lista de Comunidades Autónomas.

Están repetidas en cada provincia, por lo que una forma de evitarlo es creando un Set, que evita elementos repetidos.

...

const svgDimensions = { width: 1024, height: 1100 };
const margin = { left: 5, right: 5, top: 10, bottom: 10 };
const chartDimensions = {
  width: svgDimensions.width - margin.left - margin.right,
  height: svgDimensions.height - margin.bottom - margin.top,
};

+ const ccaaList = Array.from(new Set(data.map((d) => d.ccaa)));

const svg = d3
  .select("body")
  .append("svg")
  .attr("width", chartDimensions.width)
  .attr("height", chartDimensions.height)
  .attr("style", "background-color: #FBFAF0");

...

El siguiente paso, es crear una escala de color. D3js ya no las ofrece, pues no son recomendables tantos colores, ya que las personas no los distinguimos bien. De forma que buscamos un ejemplo de escala y lo copiamos como array.

...

const ccaaList = Array.from(new Set(data.map((d) => d.ccaa)));

+ const colorScale = [
+   "#1f77b4",
+   "#ff7f0e",
+   "#2ca02c",
+   "#d62728",
+   "#9467bd",
+   "#8c564b",
+   "#e377c2",
+   "#7f7f7f",
+   "#bcbd22",
+   "#17becf",
+   "#aec7e8",
+   "#ffbb78",
+   "#98df8a",
+   "#ff9896",
+   "#c5b0d5",
+   "#c49c94",
+   "#f7b6d2",
+   "#c7c7c7",
+   "#dbdb8d",
+   "#9edae5",
+ ];

...

La función getColor busca el índice en el array, dado un nombre de Comunidad Autónoma y se devuelve el color correspondiente:

...
const ccaaList = Array.from(new Set(data.map((d) => d.ccaa)));
const colorScale = [
  "#1f77b4",
  "#ff7f0e",
  "#2ca02c",
  "#d62728",
  "#9467bd",
  "#8c564b",
  "#e377c2",
  "#7f7f7f",
  "#bcbd22",
  "#17becf",
  "#aec7e8",
  "#ffbb78",
  "#98df8a",
  "#ff9896",
  "#c5b0d5",
  "#c49c94",
  "#f7b6d2",
  "#c7c7c7",
  "#dbdb8d",
  "#9edae5",
];

+ const getColor = (ccaa: string) => {
+   const ccaaId = ccaaList.findIndex((d) => d === ccaa);
+   return colorScale[ccaaId];
+ };

...

Finalmente, ya podemos utilizarlo en el atributo fill:

...

svg
  .selectAll("rect")
  .data(data)
  .join("rect")
  .attr("x", 0)
  .attr("y", (d, i) => i * 20)
  .attr("height", 15)
  .attr("width", (d) => scale(d.cases))
- .attr("fill", (d, i) => d3.schemeSet3[i % d3.schemeSet3.length]);
+ .attr("fill", (d, i) => getColor(d.cca));

Los colores indican que las barras tienen alguna relación. Podemos hacer que sea la Comunidad Autónoma. Para hacerlo, tenemos que ordenar los datos para que aparezcan juntas las provincias, usando una función sort:

...

svg
  .selectAll("rect")
- .data(data)
+ .data(data.sort((a, b) => a.ccaa.localeCompare(b.ccaa)))
  .join("rect")
  .attr("x", 0)
  .attr("y", (d, i) => i * 20)
  .attr("height", 15)
  .attr("width", (d) => scale(d.cases))
  .attr("fill", (d, i) => getColor(d.cca));

Añadir etiquetas

En el ejemplo anterior añadíamos un elemento SVG (rect) para cada provincia, o sea, para cada elemento del array de datos. Si se quiere añadir texto, hay que añadir dos elementos. Eso se consigue separando la selección de la función append:

const svg = d3
  .select("body")
  .append("svg")
  .attr("width", chartDimensions.width)
  .attr("height", chartDimensions.height)
  .attr("style", "background-color: #FBFAF0");

+ const province = svg
+   .selectAll("g")
+   .data(data.sort((a, b) => a.ccaa.localeCompare(b.ccaa)))
+   .join("g");

- svg
+ province
- .selectAll("rect")
- .data(data.sort((a, b) => a.ccaa.localeCompare(b.ccaa)))
- .join("rect")
+ .append("rect")
  .attr("x", 0)
  .attr("y", (d, i) => i * 20)
  .attr("height", 15)
  .attr("width", (d) => scale(d.cases))
  .attr("fill", (d, i) => getColor(d.cca));

+ province
+   .append("text")
+   .attr("x", (d) => scale(d.cases) + 5)
+   .attr("y", (d, i) => (i + 0.6) * 20)
+   .text((d) => `${d.name}: ${new Intl.NumberFormat().format(d.cases)}`);

...

La variable province tiene los la relación entre los datos y los elementos SVG, con lo que se puede usar para añadir tantos elementos como se quiera. Para que los elementos estén juntos, se ponen como hijos de un elemento g, que significa grupo. El grupo se puede mover, hacer grande o pequeño y todos los hijos cambirán a la vez. El elemento nuevo es un text, que contiene la etiqueta.

Añadir transiciones

D3js facilita hacer los gráficos dinámicos, creando efectos bonitos de forma sencilla. En nuestro caso, pasarán dos cosas: Las barras crecen hasta su medida al cargar la página y el texto aparecerá después. Para conseguirlo hay que añadir una transición a cada elemento:

En el caso de las barras, la anchura es cero, pero la transición será hasta su medida definitiva. Por defecto, la duración es corta, por lo que se ha cambiado a un segundo (1000 ms):

...

const province = svg
  .selectAll("g")
  .data(data.sort((a, b) => a.ccaa.localeCompare(b.ccaa)))
  .join("g");

province
  .append("rect")
  .attr("x", 0)
  .attr("y", (d, i) => i * 20)
  .attr("height", 15)
- .attr("width", (d) => scale(d.cases))
+ .attr("width", 0)
- .attr("fill", (d, i) => getColor(d.cca));
+ .attr("fill", (d, i) => getColor(d.cca))
+ .transition()
+ .duration(1000)
+ .attr("width", (d) => scale(d.cases));
...

En el caso del texto, jugamos con el atributo opacity que pasa de cero (transparente) a uno, totalmente opaco. La función delay hace que la transición no empiece hasta un segundo, cuando la primera parte ya se ha completado:

...
province
  .append("rect")
  .attr("x", 0)
  .attr("y", (d, i) => i * 20)
  .attr("height", 15)
  .attr("width", 0)
  .attr("fill", (d, i) => getColor(d.cca))
  .transition()
  .duration(1000)
  .attr("width", (d) => scale(d.cases));

province
  .append("text")
  .attr("x", (d) => scale(d.cases) + 5)
  .attr("y", (d, i) => (i + 0.6) * 20)
+ .attr("opacity", 0)
- .text((d) => `${d.name}: ${new Intl.NumberFormat().format(d.cases)}`);
+ .text((d) => `${d.name}: ${new Intl.NumberFormat().format(d.cases)}`)
+ .transition()
+ .delay(1000)
+ .attr("opacity", 1);

...

Finalmente vemos que Madrid no cabe dentro del elemento SVG, vamos a hacer las barras un poco más pequeñas cambiando el range de la escala:

...

const scale = d3
  .scaleLinear()
  .domain([0, d3.max(data, (d) => d.cases)])
- .range([0, chartDimensions.width - 30]);
+ .range([0, chartDimensions.width - 150]);

Aunque quedaría dar un poco de estilos, por ejemplo unas fuentes más bonitas, ya tenemos un gráfico muy presentable.

En el siguiente vídeo veremos otro tipo de layout.

¿Te apuntas a nuestro máster?

Si te ha gustado este ejemplo y tienes ganas de aprender Front End guiado por un grupo de profesionales ¿Por qué no te apuntas a nuestro Máster Front End Online Lemoncode? Tenemos tanto edición de convocatoria con clases en vivo, como edición continua con mentorización, para que puedas ir a tu ritmo y aprender mucho.