La filosofía modular de D3
Qué es D3 exactamente y qué no es: una colección de módulos independientes donde solo una minoría toca el DOM, el mapa de las familias, y por qué la curva de aprendizaje es tan pronunciada.
D3 tiene fama de biblioteca de gráficos y no lo es: no hay ninguna función que dibuje un gráfico de barras. Es una colección de unos treinta paquetes independientes, de los cuales la mayoría son matemáticas puras sobre arrays y números que funcionan igual en un worker, en Node y contra un lienzo. Entender esa separación es lo que convierte D3 de un monstruo de mil páginas de documentación en una caja de herramientas de la que coges tres.
- Distinguir los módulos de D3 que tocan el DOM de los que no.
- Situar cada familia de módulos y saber cuál resuelve cada problema.
- Explicar por qué D3 no ofrece tipos de gráfico y qué implica esa decisión.
- Reconocer código de versiones antiguas de D3 y saber por qué no funciona.
Qué es y qué no es
D3 son las siglas de Data-Driven Documents, y viene de un artículo de Michael Bostock, Vadim Ogievetsky y Jeffrey Heer presentado en InfoVis en 2011. Su antecesor directo, Protovis, era un lenguaje declarativo de marcas gráficas; D3 abandonó ese enfoque a propósito y lo sustituyó por algo más bajo nivel: manipular el DOM directamente en función de los datos.
La consecuencia de esa decisión es que D3 no tiene tipos de gráfico. No existe d3.barChart(). Lo que existe son las piezas: escalas, generadores de forma, algoritmos de disposición, formateadores, interpoladores. Un gráfico de barras se escribe combinándolas, y por eso hay tanto código de D3 en internet que hace lo mismo de veinte formas distintas.
Esto es a la vez su mayor fortaleza y el origen de toda su mala fama. La fortaleza: puedes construir cualquier representación, incluida una que nadie haya construido antes, porque no estás limitado por el catálogo de nadie. La mala fama: para hacer un gráfico de barras estándar, que es lo que necesita el noventa por ciento de la gente, escribes cien líneas donde una biblioteca de gráficos te pide cinco.
El criterio honesto para decidir si D3 es tu herramienta:
- Si necesitas tres gráficos de tipos estándar, una biblioteca de gráficos es la respuesta correcta y D3 es una pérdida de tiempo.
- Si necesitas una representación que no está en ningún catálogo, o control total sobre cada píxel, D3 es la respuesta.
- Si necesitas un sparkline, escríbelo tú: es lo que llevas haciendo desde generar rutas a partir de datos y son treinta líneas.
El mapa de los módulos
Los paquetes se publican por separado con el prefijo d3-, y el paquete d3 no es más que un índice que los reexporta todos. La división que de verdad importa no es por temas, es por si tocan el DOM o no.
| Familia | Módulos | ¿Toca el DOM? |
|---|---|---|
| Datos | d3-array, d3-dsv, d3-fetch, d3-random |
No |
| Escalas y formato | d3-scale, d3-scale-chromatic, d3-format, d3-time, d3-time-format, d3-interpolate, d3-color |
No |
| Geometría | d3-shape, d3-path, d3-polygon, d3-contour, d3-delaunay, d3-quadtree, d3-geo |
No |
| Disposiciones | d3-hierarchy, d3-force, d3-chord |
No |
| Tiempo de ejecución | d3-timer, d3-ease, d3-dispatch |
No |
| DOM | d3-selection, d3-transition, d3-axis, d3-drag, d3-zoom, d3-brush |
Sí |
Seis módulos de treinta tocan el DOM. Los otros veinticuatro son funciones puras sobre números, arrays y objetos: se pueden ejecutar en un worker, en el servidor, en una prueba unitaria sin navegador, y alimentar un lienzo 2D exactamente igual que un SVG.
Esa tabla es la lección entera del nivel. Cuando alguien dice «D3 es incompatible con React», está hablando de esos seis módulos. Los otros veinticuatro no tienen ninguna opinión sobre quién dibuja.
De la familia de datos, cuatro funciones de d3-array justifican el paquete por sí solas y casi nadie las conoce:
import { extent, bin, rollup, bisector, fsum } from 'd3-array';
extent(datos, d => d.valor); // [min, max] en una pasada
bin().thresholds(20)(valores); // histograma: bins con x0, x1
rollup(datos, v => v.length, d => d.pais); // agrupar y agregar, devuelve un Map
bisector(d => d.fecha).left; // busqueda binaria sobre array ordenado
fsum(valores); // suma sin error acumulado de coma flotante
extent es la que sustituye al par Math.min/Math.max con un solo recorrido y sin el desbordamiento de pila que produce Math.max(...array) con cien mil elementos. fsum usa el algoritmo de suma compensada de Neumaier para que sumar un millón de decimales no derive. bisector es la búsqueda binaria que necesita el hit testing de qué es una escala, ya escrita y probada.
Y una que cierra el círculo del nivel anterior: d3.ticks y d3.tickIncrement, que viven en d3-array, son exactamente el algoritmo de las marcas bonitas. Si ya lo has implementado, no necesitas el paquete; si no, ahí está.
Por qué la curva es tan pronunciada
D3 es difícil, y conviene ser exacto sobre por qué, porque casi todas las explicaciones que circulan son incorrectas.
No es difícil por las matemáticas. scaleLinear es la interpolación lineal que ya sabes escribir. d3.line() genera una cadena de d que ya sabes leer desde la anatomía del atributo d.
Es difícil porque son dos bibliotecas en una. d3-selection es una API de manipulación del DOM completa, con su propio modelo de selección, su propio sistema de eventos y su propia semántica de encadenado. Aprender D3 por el camino clásico significa aprender esa segunda API del DOM antes de llegar a la parte que te interesaba. Y esa API compite con la que ya usas: si tu aplicación es React, Svelte o Vue, ya tienes un dueño del DOM.
Es difícil porque el encadenamiento oculta el tipo. En svg.selectAll('rect').data(datos).join('rect').attr('x', d => x(d.mes)), cada eslabón devuelve un objeto distinto con métodos distintos, y no hay forma de saber cuál sin conocer la biblioteca. Un error de orden produce un objeto vacío en silencio, sin excepción y sin nada dibujado.
Es difícil porque la documentación está repartida por paquetes. Cada módulo tiene su propia referencia, y una tarea sencilla («eje temporal con etiquetas en español») cruza cuatro: d3-scale, d3-time, d3-time-format y d3-axis.
Nada de eso es un defecto de diseño: es el precio de la modularidad. Pero explica por qué el camino corto es aprender d3-scale, d3-shape y d3-array, que se leen en una tarde, y dejar d3-selection para cuando de verdad haga falta.
Buscas «d3 bar chart» y encuentras diez mil ejemplos. La inmensa mayoría son de la época de los bloques de código compartidos, es decir, de D3 versión 3 y 4, y no funcionan. Lo cruel es que no fallan con un mensaje de versión: fallan con un undefined is not a function en la línea equivocada, o peor, no fallan y no dibujan nada.
Los cuatro cambios incompatibles que tienes que saber reconocer, porque son los que separan los ejemplos vivos de los muertos:
El aplanado del espacio de nombres, en la versión 4. Antes era d3.scale.linear(), d3.time.format(), d3.layout.tree(). Ahora es d3.scaleLinear(), d3.timeFormat(), d3.tree(). Si ves un punto de más en el medio, el ejemplo es de 2016 o anterior. Este es el único que falla ruidosamente.
Las promesas en la carga de datos, en la versión 5. d3.csv('datos.csv', function(error, datos) { ... }) pasó a ser d3.csv('datos.csv').then(datos => ...). Un ejemplo antiguo pasa la función como segundo argumento, donde la API actual espera una función de conversión de fila. El resultado es que se ejecuta como conversor sobre cada fila, y lo que llega al gráfico es basura. Sin error.
La desaparición de d3.event, en la versión 6. Antes, dentro de un manejador se leía el evento de la variable global d3.event. Ahora el evento llega como primer argumento del manejador y el dato como segundo. Un ejemplo antiguo hace function(d) { ... d3.event.pageX ... } y en D3 actual esa d es el evento y d3.event no existe. El tooltip aparece en la esquina superior izquierda y nadie entiende por qué.
Solo módulos ES, en la versión 7. El paquete d3 dejó de publicar la variante para require. En un proyecto de Node en CommonJS, require('d3') falla y hay que usar importación dinámica o pasar el proyecto a módulos ES. Algunos paquetes d3-* individuales siguen ofreciendo más formatos, lo que hace que el fallo dependa de qué importes.
La regla práctica que ahorra horas: cuando busques un ejemplo, filtra por fecha y descarta todo lo anterior a 2021. Y cuando encuentres uno perfecto de 2015, no intentes migrarlo eslabón a eslabón: entiéndelo, tira el código y reescríbelo. Migrar un ejemplo antiguo de D3 cuesta sistemáticamente más que escribirlo de nuevo, porque los cambios no son sintácticos sino de modelo.
Coge los tres primeros resultados de buscar «d3 line chart» y clasifícalos por versión usando las cuatro pistas del callout, sin ejecutarlos. Después ejecuta el que hayas clasificado como más antiguo con D3 actual y comprueba si el fallo se manifiesta donde has predicho. Esa media hora es la vacuna contra las próximas veinte horas perdidas.