Fecha publicación: 26 may 2021

Usando el layout treemap

En este vídeo veremos como usar uno de los layouts que d3js ofrece. Treemap representa datos jerárquicos en forma de rectángulos anidados. Cada rectángulo tiene un área proporcional al valor. En nuestro caso, cuantos más casos de COVID-19 tenga una provincia, más grande será su rectángulo y los rectángulos estarán agrupados por Comunidad Autónoma.

Un layout en d3 es una función que da las posiciones y formas de los elementos a dibujar. Como se hace esto, depende del usuario.

Preparando los datos

Otra vez partiremos del proyecto 00-boiler y añadiremos el siguiente código sacado del vídeo anterior

import * as d3 from "d3";

const svgDimensions = { width: 1024, height: 768 };
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,
};

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

+ 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");

Los datos hay que procesarlos de forma que tengan la estructura que necesita el layout:

...

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

+ interface TreemapData {
+   name: string;
+   children?: TreemapData[];
+   value?: number;
+ }

+ const provData: TreemapData[] = ccaaList.map((ccaa) => ({
+   name: ccaa,
+   children: data
+     .filter((prov) => prov.ccaa === ccaa)
+     .map((prov) => ({ name: prov.name, value: prov.cases })),
+ }));

+ const treemapData: TreemapData = { name: "España", children: provData };

...

La interfaz TreemapData es recursiva, como se puede ver. Los elementos tienen un nombre, que será España en el nivel raíz, el nombre de las Comunidades en sus hijos y el nombre de las provincias en los nietos. De esta forma, las funciones de d3 podrán saber si ya están en el último nivel (tienen el elemento valor y no tienen hijos).
Nótese que el elemento children es del mismo tipo TreemapData, por lo que el objeto se puede anidar tantas veces como se quiera.

Definiendo el layout

Para que el layout treemap funcione, necesita que los datos vengan en una estructura llamada hierarchy. El tipo que devuelve incluye campos como los datos de nuestro nodo, el padre del nodo, sus hijos, etc.

...

const treemapData: TreemapData = { name: "España", children: provData };

+ const hierarchy = d3
+   .hierarchy(treemapData)
+   .sum((d) => d.value)
+   .sort((a, b) => b.value - a.value);

...

sum indica como calcular el valor total (de casos COVID en nuestro ejemplo) sort hará que los rectángulos aparezcan ordenados de más casos a menos casos

Ya podemos crear el layout. root contendrá un array con todos los datos necesarios para representar los rectángulos. Hay que indicar el tipo a TypeScript o se pierde y da errores a la hora de dibujar.

...

const hierarchy = d3
  .hierarchy(treemapData)
  .sum((d) => d.value)
  .sort((a, b) => b.value - a.value);

+ const treemap = d3
+   .treemap()
+   .size([chartDimensions.width, chartDimensions.height]);

+ const root = treemap(hierarchy) as d3.HierarchyRectangularNode<TreemapData>;

...
  

Dibujando

Como en el ejemplo anterior, se añade un grupo para cada provincia. root.leaves() representa todas las provincias (ya que son el último nivel) con las posiciones para dibujar, por lo que se puede crear el svg:

...

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

+ const leaf = svg
+   .selectAll("g")
+   .data(root.leaves())
+   .join("g")
+   .attr("transform", (d) => `translate(${d.x0}, ${d.y0})`);

+ leaf
+   .append("rect")
+   .attr("width", (d) => d.x1 - d.x0)
+   .attr("height", (d) => d.y1 - d.y0)

Los elementos contienen la variable x0 e y0 que situan el origen del rectángulo.

El rectángulo se puede dibujar calculando la anchura y altura, ya que tenemos las coordenadas.

Dentro del elemento, podemos mirar el padre, que será la comunidad autónoma que nos permite calcular el color.

Si ejecutamos ahora el código, veríamos una primera versión donde los rectángulos salen en negro, aquí podríamos aplicar la función de getColor que hicimos en el vídeo anterior:

...

+ 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];
+ };

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

const leaf = svg
  .selectAll("g")
  .data(root.leaves())
  .join("g")
  .attr("transform", (d) => `translate(${d.x0}, ${d.y0})`);

leaf
  .append("rect")
  .attr("width", (d) => d.x1 - d.x0)
  .attr("height", (d) => d.y1 - d.y0)
+ .attr("fill", (d) => getColor(d.parent.data.name));

Ahora ya si que podemos añadir el texto con los nombres de las provincias.

...

leaf
  .append("rect")
  .attr("width", (d) => d.x1 - d.x0)
  .attr("height", (d) => d.y1 - d.y0)
  .attr("fill", (d) => getColor(d.parent.data.name));

+ leaf.append("title").text((d) => `${d.data.name}: ${d.data.value}`);

+ leaf
+   .append("text")
+   .attr("x", 3)
+   .attr("y", 15)
+   .text((d) => d.data.name);

+ leaf
+   .append("text")
+   .attr("x", 3)
+   .attr("y", 35)
+   .text((d) => new Intl.NumberFormat().format(d.data.value));
  

El texto se puede colocar de forma sencilla, ya que el grupo lo tiene ya situado

Si queremos añadir el valor con los casos de COVID-19:

...

leaf
  .append("text")
  .attr("x", 3)
  .attr("y", 15)
  .text((d) => d.data.name);

+ leaf
+   .append("text")
+   .attr("x", 3)
+   .attr("y", 35)
+   .text((d) => new Intl.NumberFormat().format(d.data.value));
  

Se añade otro texto diferente al anterior porque hay poco espacio a la derecha del mismo.

Ya tenemos una representación básica del layout Treemap, como puntos de mejora se podría implementar:

  • Mejorar el estilado de las fuentes.
  • En provincias donde hay pocos datos, por ejemplo: Ceuta, Soria, etc, se corta. En ejemplos más complejos se puede hacer que haciendo click en el cuadrado se vea toda la Comunidad Autónoma con un zoom.
  • Proveer los valores hardcoded mediante constantes o variables de entorno para que sean fácilmente configurables

¿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.