Usando el layout circle packing

Este layout se parece bastante al anterior pero usa círculos. Acostumbra a ser más atractivo aunque distinguir los valores es más difícil. Vamos a ver como se usaría en este caso.

Preparando los datos

Éste caso es idéntico al anterior en cuanto a los datos. La única diferencia es el tipo, que es HierarchyCircularNode. Te da el centro y radio del círculo en lugar del la coordenadas x e y de los rectángulos. Fácil!

Partiremos del ejemplo anterior Treemap pero necesita algunas modificaciones:

...

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 pack = d3
+ .pack()
+ .size([chartDimensions.width, chartDimensions.height])
+ .padding(1)

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

...

Dibujando

La primera diferencia es que en este caso los círculos se dibujarán sea cual sea el nivel. O sea, uno grande para toda España, dentro otros para cada Comunidad Autónoma y finalmente las provincias. Por lo tanto, no usaremos leaf() sino todos los descendientes

...

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())
+ .data(d3.group(root.descendants(), (d) => d.height))
  .join("g")
+ .selectAll("g")
+ .data((d) => d[1])
+ .join("g")
- .attr("transform", (d) => `translate(${d.x0}, ${d.y0})`);
+ .attr("transform", (d) => `translate(${d.x + 1},${d.y + 1})`);

...

La función d3.group() separará los elementos agrupándolos por la propiedad height. El elemento padre (España) tiene height = 2, Comunidades Autónomas 1 y provincias 0. Por lo tanto, habrá tres elementos que crearán los tres niveles de círculo.

Después, se usan los datos dentro de cada elemento para crear los grupos: .data(d => d[1]). Si uno se fija bien, la función anterior devuelve el númedo de value y los elementos que hay dentro, por lo que usamos d[1] para conseguir los elementos.

El resto se parece mucho al ejemplo anterior pero dibujando círculos. En este caso, los colores no pueden depender solo de la provincia, porque queremos pintar las Comunidades Autónomas también. Para ello, la función del color será:

...

- const getColor = (ccaa: string) => {
+ const getColor = (ccaa: string, isCCAA: boolean) => {
  const ccaaId = ccaaList.findIndex((d) => d === ccaa);
+ const ccaaColor = colorScale[ccaaId];

- return colorScale[ccaaId];
+ return isCCAA
+   ? ccaaColor
+   : (d3.color(ccaaColor) ?? d3.color("#000000"))
+       .brighter(Math.random())
+       .formatHex();
};
  
...

const leaf = svg
  .selectAll("g")
  .data(d3.group(root.descendants(), (d) => d.height))
  .join("g")
  .selectAll("g")
  .data((d) => d[1])
  .join("g")
  .attr("transform", (d) => `translate(${d.x + 1},${d.y + 1})`);

- 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("circle")
+   .attr("r", (d) => d.r)
+   .attr(
+     "fill",
+     (d, i) =>
+       d.depth > 0 &&
+       getColor(d.depth === 1 ? d.data.name : d.parent.data.name, d.depth === 1)
+   )
+   .attr("stroke", "#000000")
+   .attr("stroke-width", 0.5);

...

Vemos que tenemos que decir si el elemento es Comunidad Autónoma o no y a que comunidad pertenece. Así, si es una provincia, le cambiamos el brillo al color de forma aleatoria, por lo que los círculos serán de la misma familia.

España queda en negro, pues no encaja en la fórmula, pero el resultado está bien por lo que lo dejamos.

Depth, al igual que value, te dice el nivel dentro de la estructura. 1 es para Comunidad Autónoma, por lo que se puede devolver el nombre o ir al padre y devolver el nombre en caso que estemos en una provincia.

Aquí, añadir etiquetas es un poco más complicado, es otra de las limitaciones de este layout aunque el resultado si que parece más atractivo. Pero podemos añadir el elemento title que nos permite mostrar el nombre de las provincias y el valor de los casos de COVID-19 en un efecto hover:

...
  
leaf
  .append("circle")
  .attr("r", (d) => d.r)
  .attr(
    "fill",
    (d, i) =>
      d.depth > 0 &&
      getColor(d.depth === 1 ? d.data.name : d.parent.data.name, d.depth === 1)
  )
  .attr("stroke", "#000000")
  .attr("stroke-width", 0.5);

+ leaf
+   .append("title")
+   .text(
+     (d) =>
+       `${d.data.name} ${
+         d.depth === 2 ? new Intl.NumberFormat().format(d.data.value) : ""
+       }`
+   );


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

Este layout se suele utilizar en periódicos y presentaciones del estilo, aunque habría que mejorarlo un poco para que se pudieran comprender mejor los datos. Pero ya es una opción bastante avanzada y bonita.

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