Introducción a D3
¿Qué es D3?
D3 (Data-Driven Documents) es una biblioteca de JavaScript para crear visualizaciones manipulando directamente elementos del DOM — principalmente SVG — a partir de datos (Bostock et al., 2011). A diferencia de Observable Plot, que genera gráficos completos de forma declarativa, D3 ofrece un enfoque imperativo: el programador indica paso a paso qué elementos crear, dónde posicionarlos y cómo estilizarlos.
Declarativo vs Imperativo
| Aspecto | Observable Plot (declarativo) | D3 (imperativo) |
|---|---|---|
| Filosofía | Describe qué quieres ver | Indica cómo construirlo paso a paso |
| Ejes y escalas | Automáticos | Se crean manualmente |
| SVG | Generado internamente | Tú lo creas y controlas |
| Curva de aprendizaje | Baja | Alta |
| Flexibilidad | Limitada a lo que Plot ofrece | Total |
| Mejor para | Exploración rápida, gráficos estándar | Visualizaciones personalizadas, animaciones |
Ambas bibliotecas están disponibles globalmente en Observable Framework. La variable d3 se puede usar directamente en cualquier bloque de código sin necesidad de importarla.
En esta lección aprenderemos a:
- Crear elementos SVG con D3
- Encadenar métodos para construir visualizaciones paso a paso
- Vincular datos a elementos visuales con el patrón data join
- Traducir datos a píxeles con escalas
- Generar ejes automáticamente
- Construir un gráfico de barras completo desde cero
Creando SVG con D3
En la lección anterior escribimos SVG directamente en HTML. Con D3, creamos los mismos elementos usando JavaScript:
Ver código D3 rectángulo
d3.create("svg")
.attr("width", 400)
.attr("height", 120)
.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 200)
.attr("height", 80)
.attr("fill", "#9B1C26")
.node()
¿Qué hace cada parte?
- d3.create("svg"): crea un elemento
<svg>en memoria (no lo muestra todavía) - .attr("width", 400): establece el atributo
widthdel SVG - .append("rect"): agrega un elemento
<rect>dentro del SVG - .attr("x", 20): establece la posición X del rectángulo
- .node(): extrae el elemento del DOM para que Observable Framework lo muestre
El resultado es idéntico al SVG manual de la lección anterior, pero construido con JavaScript. La diferencia es que ahora los valores pueden venir de variables y datos en lugar de estar escritos a mano.
Encadenamiento de métodos
D3 usa un patrón llamado method chaining (encadenamiento de métodos): cada método devuelve el elemento modificado, lo que permite agregar múltiples atributos en una sola expresión fluida.
Ver código D3 encadenamiento
d3.create("svg")
.attr("width", 400)
.attr("height", 120)
.call((svg) => {
svg
.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 200)
.attr("height", 80)
.attr("fill", "#9B1C26")
.attr("fill-opacity", 0.85)
svg
.append("text")
.attr("x", 230)
.attr("y", 68)
.attr("font-size", 14)
.attr("fill", "#333")
.text("Chile: 80.0 años")
})
.node()
¿Qué cambió?
- .call(svg => { ... }): ejecuta una función recibiendo el SVG como argumento, útil para agregar múltiples elementos sin perder la referencia al contenedor
- .append("text"): agrega un elemento
<text>al SVG - .text("Chile: 80.0 años"): establece el contenido de texto del elemento
Hasta aquí seguimos escribiendo valores a mano. La verdadera potencia de D3 aparece cuando esos valores vienen de los datos.
Construyendo con D3: los primitivos SVG
En la lección anterior aprendimos los elementos SVG escribiéndolos directamente en HTML. Ahora veamos cómo crear esos mismos elementos con D3, usando .append() y .attr().
Grupos y translate
El elemento <g> se crea con .append("g") y se posiciona con el atributo transform:
Ver código D3 grupos y translate
{
const svg = d3.create("svg").attr("width", 400).attr("height", 150)
// Primer grupo posicionado en (50, 30)
const grupo = svg.append("g").attr("transform", "translate(50, 30)")
grupo
.append("rect")
.attr("width", 120)
.attr("height", 60)
.attr("fill", "#9B1C26")
grupo
.append("text")
.attr("x", 60)
.attr("y", 35)
.attr("text-anchor", "middle")
.attr("fill", "white")
.attr("font-size", 14)
.text("Grupo 1")
// Segundo grupo posicionado en (220, 30)
const grupo2 = svg.append("g").attr("transform", "translate(220, 30)")
grupo2
.append("rect")
.attr("width", 120)
.attr("height", 60)
.attr("fill", "#9B1C26")
.attr("fill-opacity", 0.5)
grupo2
.append("text")
.attr("x", 60)
.attr("y", 35)
.attr("text-anchor", "middle")
.attr("fill", "white")
.attr("font-size", 14)
.text("Grupo 2")
display(svg.node())
}
¿Qué hace cada parte?
- svg.append("g"): crea un grupo
<g>dentro del SVG y devuelve una referencia a él - const grupo = ...: al guardar la referencia en una variable, podemos agregar elementos dentro del grupo usando
grupo.append() - .attr("transform", "translate(50, 30)"): desplaza todo el grupo a la posición (50, 30)
- Los elementos dentro del grupo (
rect,text) usan coordenadas relativas al grupo, no al SVG
Esto es idéntico al SVG manual de la lección anterior, pero con una ventaja: las posiciones y atributos ahora pueden venir de variables en lugar de estar escritas a mano.
Círculos, líneas y texto
Veamos cómo crear los demás primitivos con D3, combinándolos en un mini scatter plot estático:
Ver código D3 círculos, líneas y texto
{
const svg = d3.create("svg").attr("width", 400).attr("height", 200)
const g = svg.append("g").attr("transform", "translate(50, 20)")
// Líneas de ejes
g.append("line")
.attr("x1", 0)
.attr("y1", 160)
.attr("x2", 320)
.attr("y2", 160)
.attr("stroke", "#ccc")
.attr("stroke-width", 1)
g.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", 160)
.attr("stroke", "#ccc")
.attr("stroke-width", 1)
// Tres puntos con diferentes posiciones y tamaños
g.append("circle")
.attr("cx", 80)
.attr("cy", 40)
.attr("r", 8)
.attr("fill", "#9B1C26")
.attr("fill-opacity", 0.7)
g.append("circle")
.attr("cx", 160)
.attr("cy", 90)
.attr("r", 12)
.attr("fill", "#9B1C26")
.attr("fill-opacity", 0.7)
g.append("circle")
.attr("cx", 260)
.attr("cy", 60)
.attr("r", 6)
.attr("fill", "#9B1C26")
.attr("fill-opacity", 0.7)
// Etiquetas de ejes
g.append("text")
.attr("x", 160)
.attr("y", 180)
.attr("text-anchor", "middle")
.attr("font-size", 12)
.attr("fill", "#666")
.text("Eje X")
g.append("text")
.attr("x", -10)
.attr("y", 80)
.attr("text-anchor", "middle")
.attr("font-size", 12)
.attr("fill", "#666")
.attr("transform", "rotate(-90, -10, 80)")
.text("Eje Y")
display(svg.node())
}
¿Qué hace cada parte?
- .append("line"): crea una línea con
x1,y1,x2,y2(igual que en SVG manual) - .append("circle"): crea un círculo con
cx,cy,r - .append("text").text("..."): crea un texto y define su contenido con
.text() - transform: "rotate(-90, -10, 80)": rota el texto del eje Y 90 grados alrededor del punto (-10, 80)
El patrón que se repite
Todos los primitivos siguen la misma estructura en D3:
contenedor.append("tipo_elemento")
.attr("atributo1", valor)
.attr("atributo2", valor)
...
Lo único que cambia son los atributos específicos de cada elemento (cx/cy para círculos, x1/y1 para líneas, etc.). Una vez que conoces el patrón, crear cualquier elemento SVG con D3 es mecánico.
El problema de este enfoque
Este scatter plot tiene 3 puntos hardcodeados. Si tuviéramos 195 países, necesitaríamos escribir 195 veces .append("circle") con coordenadas diferentes. Eso es exactamente lo que el data join resuelve en la siguiente sección.
Data join: vinculando datos a elementos
El data join es el concepto central de D3: toma un array de datos y crea un elemento SVG por cada elemento del array. Es lo que convierte a D3 en una herramienta de visualización y no solo una biblioteca de dibujo.
Ver código D3 data join
{
const svg = d3.create("svg").attr("width", 400).attr("height", 200)
svg
.selectAll("rect")
.data([80.0, 77.8, 77.1, 76.8, 76.5])
.join("rect")
.attr("x", 0)
.attr("y", (d, i) => i * 38)
.attr("width", (d) => d * 4)
.attr("height", 30)
.attr("fill", "#9B1C26")
.attr("fill-opacity", 0.85)
display(svg.node())
}
¿Qué hace cada parte?
- svg.selectAll("rect"): selecciona todos los rectángulos existentes (al inicio no hay ninguno)
- .data([80.0, 77.8, 77.1, 76.8, 76.5]): vincula el array de datos a la selección
- .join("rect"): crea un
<rect>por cada elemento del array que no tenga un rectángulo asignado - .attr("y", (d, i) => i * 38): posiciona cada rectángulo verticalmente; d es el dato (ej: 80.0), i es el índice (0, 1, 2...)
- .attr("width", d => d * 4): el ancho de cada barra es proporcional al valor
La función (d, i) => ...
Dentro de un data join, cada método .attr() puede recibir una función en lugar de un valor fijo. Esta función recibe dos argumentos:
- d: el dato vinculado a ese elemento (ej:
80.0,77.8) - i: el índice del elemento en el array (ej:
0,1,2)
Esto es lo que hace que D3 sea "Data-Driven": los atributos visuales se calculan a partir de los datos.
Usando datos reales del CSV
En lugar de un array manual, vinculemos los datos de nuestro CSV. La variable sudamerica ya contiene los países filtrados y ordenados:
Ver código D3 con datos del CSV
{
const svg = d3
.create("svg")
.attr("width", 500)
.attr("height", sudamerica.length * 38)
// Una barra por cada país
svg
.selectAll("rect")
.data(sudamerica)
.join("rect")
.attr("x", 100)
.attr("y", (d, i) => i * 38)
.attr("width", (d) => d.life_expectancy * 4)
.attr("height", 30)
.attr("fill", "#9B1C26")
.attr("fill-opacity", 0.85)
// Una etiqueta por cada país
svg
.selectAll("text")
.data(sudamerica)
.join("text")
.attr("x", 95)
.attr("y", (d, i) => i * 38 + 20)
.attr("text-anchor", "end")
.attr("font-size", 13)
.attr("fill", "#333")
.text((d) => d.country)
display(svg.node())
}
¿Qué cambió?
- .data(sudamerica): ahora vinculamos objetos completos del CSV, no valores sueltos
- d.life_expectancy: accedemos a las propiedades del objeto, igual que en Observable Plot
- d.country: usamos el nombre del país para las etiquetas
- sudamerica.length * 38: el alto del SVG se adapta al número de países
- Se hacen dos data joins independientes: uno para rectángulos y otro para textos
El gráfico ya muestra todos los países con sus valores reales. Pero los anchos de las barras siguen calculándose con una regla de tres manual (d.life_expectancy * 4). Esto funciona para este caso, pero no es robusto: si los datos cambian de rango, las barras podrían salirse del SVG o ser demasiado pequeñas. Las escalas de D3 resuelven este problema.
Escalas: traduciendo datos a píxeles
Las escalas de D3 son funciones que traducen valores de datos a valores visuales. Reciben un domain (rango de datos) y un range (rango de píxeles) y devuelven una función de conversión.
Este concepto ya lo conocemos de Observable Plot: cuando escribíamos r: { range: [2, 20] } o color: { domain: [50, 85] }, Plot creaba escalas internamente. En D3, las creamos explícitamente.
d3.scaleLinear: valores continuos
Traduce un rango numérico continuo a otro. Es la escala más usada para posiciones y tamaños.
Ver código escala lineal
const escalaX = d3
.scaleLinear()
.domain([0, 85]) // Valores de datos: 0 a 85 años
.range([0, 350]) // Valores en píxeles: 0 a 350px
¿Qué hace?
- domain([0, 85]): los datos van de 0 a 85 (años de expectativa de vida)
- range([0, 350]): el espacio visual disponible va de 0 a 350 píxeles
- escalaX(80) devuelve 329: 80 años se convierten en 329 píxeles
- La conversión es lineal:
píxeles = (valor / 85) * 350
Es exactamente el cálculo que hicimos a mano en la lección de SVG (valor / 85 * 350), pero encapsulado en una función reutilizable.
d3.scaleBand: categorías en un eje
Distribuye categorías (países, continentes, meses) equitativamente en un espacio disponible, calculando la posición y el ancho de cada banda.
Ver código escala de bandas
const escalaY = d3
.scaleBand()
.domain(sudamerica.map((d) => d.country)) // Lista de países
.range([0, 400]) // Espacio vertical disponible
.padding(0.2) // 20% de espacio entre bandas
¿Qué hace?
- domain(sudamerica.map(d => d.country)): la lista de categorías (nombres de países)
- range([0, 400]): el espacio vertical total disponible
- padding(0.2): separación entre bandas (20% del ancho de cada banda)
- escalaY("Chile"): devuelve la posición Y donde comienza la banda de Chile
- escalaY.bandwidth(): devuelve el alto que debe tener cada barra
Esto elimina la necesidad de calcular i * 38 manualmente: la escala distribuye los países equitativamente y calcula el espaciado.
Aplicando escalas al gráfico
Ahora reemplazamos los cálculos manuales por las escalas:
Ver código D3 con escalas
{
const width = 500
const height = 400
const margin = { top: 10, right: 40, bottom: 10, left: 100 }
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom
// Escala horizontal: expectativa de vida → píxeles
const x = d3
.scaleLinear()
.domain([0, d3.max(sudamerica, (d) => d.life_expectancy)])
.range([0, innerWidth])
// Escala vertical: países → posiciones
const y = d3
.scaleBand()
.domain(sudamerica.map((d) => d.country))
.range([0, innerHeight])
.padding(0.2)
const svg = d3.create("svg").attr("width", width).attr("height", height)
// Grupo principal desplazado por los márgenes
const g = svg
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
// Barras
g.selectAll("rect")
.data(sudamerica)
.join("rect")
.attr("x", 0)
.attr("y", (d) => y(d.country))
.attr("width", (d) => x(d.life_expectancy))
.attr("height", y.bandwidth())
.attr("fill", "#9B1C26")
.attr("fill-opacity", 0.85)
// Etiquetas de países
g.selectAll(".label")
.data(sudamerica)
.join("text")
.attr("class", "label")
.attr("x", -10)
.attr("y", (d) => y(d.country) + y.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.attr("font-size", 13)
.attr("fill", "#333")
.text((d) => d.country)
display(svg.node())
}
¿Qué cambió?
- margin: objeto con los márgenes del gráfico, un patrón estándar en D3 para reservar espacio para ejes y etiquetas
- innerWidth / innerHeight: el área de dibujo real después de descontar los márgenes
- d3.max(sudamerica, d => d.life_expectancy): calcula el valor máximo del array para el dominio de la escala
- y(d.country): la escala de bandas devuelve la posición Y del país
- y.bandwidth(): el alto de cada barra lo calcula la escala automáticamente
- x(d.life_expectancy): la escala lineal convierte el valor a píxeles
<g>con translate: crea el espacio interior desplazado por los márgenes
Los datos ahora controlan completamente las dimensiones visuales a través de las escalas.
Ejes: generación automática
Los ejes son uno de los componentes más tediosos de crear manualmente: requieren líneas, ticks, etiquetas y formato numérico. D3 los genera automáticamente a partir de una escala.
Ver código D3 con ejes
{
const width = 500
const height = 430
const margin = { top: 10, right: 40, bottom: 30, left: 100 }
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom
const x = d3
.scaleLinear()
.domain([0, d3.max(sudamerica, (d) => d.life_expectancy)])
.range([0, innerWidth])
.nice()
const y = d3
.scaleBand()
.domain(sudamerica.map((d) => d.country))
.range([0, innerHeight])
.padding(0.2)
const svg = d3.create("svg").attr("width", width).attr("height", height)
const g = svg
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
// Eje X (inferior)
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(x).ticks(5))
// Eje Y (izquierdo)
g.append("g").call(d3.axisLeft(y))
// Barras
g.selectAll("rect")
.data(sudamerica)
.join("rect")
.attr("x", 0)
.attr("y", (d) => y(d.country))
.attr("width", (d) => x(d.life_expectancy))
.attr("height", y.bandwidth())
.attr("fill", "#9B1C26")
.attr("fill-opacity", 0.85)
display(svg.node())
}
¿Qué cambió?
- d3.axisBottom(x): crea un eje horizontal a partir de la escala
x, generando línea, ticks y etiquetas automáticamente - d3.axisLeft(y): crea un eje vertical a partir de la escala
y - .call(): aplica el generador de ejes a un grupo
<g>(el eje dibuja sus elementos dentro del grupo) - .ticks(5): sugiere aproximadamente 5 marcas en el eje X
- .nice(): redondea el dominio a valores "bonitos" (ej: 82 → 85)
- translate(0, innerHeight): el eje inferior se posiciona en la base del área de dibujo
Ya no necesitamos crear las etiquetas de países manualmente: el eje izquierdo las genera a partir de la escala de bandas.
Gráfico completo: todas las piezas juntas
Agreguemos los elementos finales: título, valores al final de las barras y una línea base:
Ver código D3 gráfico completo
{
const width = 550
const height = 480
const margin = { top: 40, right: 50, bottom: 30, left: 100 }
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom
// Escalas
const x = d3
.scaleLinear()
.domain([0, d3.max(sudamerica, (d) => d.life_expectancy)])
.range([0, innerWidth])
.nice()
const y = d3
.scaleBand()
.domain(sudamerica.map((d) => d.country))
.range([0, innerHeight])
.padding(0.2)
// Contenedor SVG
const svg = d3.create("svg").attr("width", width).attr("height", height)
// Título
svg
.append("text")
.attr("x", width / 2)
.attr("y", 22)
.attr("text-anchor", "middle")
.attr("font-size", 16)
.attr("font-weight", "bold")
.attr("fill", "#333")
.text("Expectativa de vida en Sudamérica")
// Grupo principal con márgenes
const g = svg
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
// Eje X (inferior, sin línea de dominio)
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(x).ticks(5))
.call((g) => g.select(".domain").remove())
// Eje Y (izquierdo, sin línea de dominio)
g.append("g")
.call(d3.axisLeft(y))
.call((g) => g.select(".domain").remove())
// Línea base
g.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", innerHeight)
.attr("stroke", "#ccc")
.attr("stroke-width", 1)
// Barras
g.selectAll("rect")
.data(sudamerica)
.join("rect")
.attr("x", 0)
.attr("y", (d) => y(d.country))
.attr("width", (d) => x(d.life_expectancy))
.attr("height", y.bandwidth())
.attr("fill", "#9B1C26")
.attr("fill-opacity", 0.85)
// Valores al final de cada barra
g.selectAll(".value")
.data(sudamerica)
.join("text")
.attr("class", "value")
.attr("x", (d) => x(d.life_expectancy) + 5)
.attr("y", (d) => y(d.country) + y.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("font-size", 12)
.attr("fill", "#333")
.text((d) => d.life_expectancy)
display(svg.node())
}
¿Qué se agregó?
- Título: un
<text>posicionado centrado en la parte superior del SVG - .call(g => g.select(".domain").remove()): elimina la línea gruesa que los ejes dibujan por defecto (la "domain line"), dejando solo los ticks y etiquetas
- Línea base: una línea vertical gris en x=0 como referencia visual
- Valores: un data join de textos posicionados al final de cada barra con x(d.life_expectancy) + 5 para dejarlos justo después de la barra
- dy="0.35em": centra verticalmente el texto respecto al punto de anclaje (truco estándar en D3)
Comparación: el mismo gráfico en Plot vs D3
Ver código Plot.plot equivalente
Plot.plot({
width: 550,
height: 480,
marginLeft: 100,
marks: [
Plot.barX(sudamerica, {
y: "country",
x: "life_expectancy",
fill: "#9B1C26",
fillOpacity: 0.85,
sort: { y: "-x" },
}),
Plot.ruleX([0]),
Plot.text(sudamerica, {
y: "country",
x: "life_expectancy",
text: (d) => `${d.life_expectancy}`,
textAnchor: "start",
dx: 5,
fontSize: 12,
fill: "#333",
}),
],
})
La diferencia en números
| Aspecto | Observable Plot | D3 |
|---|---|---|
| Líneas de código | ~20 | ~60 |
| Escalas | Automáticas | Creadas manualmente |
| Ejes | Automáticos | Creados y posicionados manualmente |
| Márgenes | marginLeft: 100 |
Objeto margin + cálculos de innerWidth/innerHeight |
| Ordenamiento | sort: {y: "-x"} |
Pre-ordenar el array antes de bindear |
| Data binding | Implícito en la marca | selectAll → data → join explícito |
Entonces, ¿por qué usar D3? Porque el código de D3 nos da control sobre cada píxel. Podemos agregar animaciones, interacciones complejas, layouts que Plot no ofrece, o personalizar cualquier aspecto visual sin estar limitados por la API de una marca. La próxima lección explorará precisamente esas capacidades.
Resumen de conceptos D3
| Concepto | Método | Función |
|---|---|---|
| Crear SVG | d3.create("svg") |
Crea un elemento SVG en memoria |
| Agregar elemento | .append("tipo") |
Agrega un hijo al elemento actual |
| Establecer atributo | .attr("nombre", valor) |
Define atributos SVG (posición, tamaño, color) |
| Establecer texto | .text(valor) |
Define el contenido de un <text> |
| Seleccionar | d3.select() / d3.selectAll() |
Selecciona elementos del DOM |
| Vincular datos | .data(array) |
Asocia un array de datos a una selección |
| Crear por dato | .join("tipo") |
Crea un elemento por cada dato del array |
| Extraer nodo | .node() |
Devuelve el elemento del DOM para mostrarlo |
Resumen de escalas
| Escala | Método D3 | Equivalente en Plot | Mejor para |
|---|---|---|---|
| Lineal | d3.scaleLinear() |
type: "linear" (default) |
Valores numéricos continuos |
| Bandas | d3.scaleBand() |
Automática en barras | Categorías equiespaciadas |
| Logarítmica | d3.scaleLog() |
type: "log" |
Valores con rangos extremos (PIB) |
| Ordinal | d3.scaleOrdinal() |
color: { domain, range } |
Mapear categorías a colores |
| Tiempo | d3.scaleTime() |
type: "time" |
Fechas y series temporales |
Todas las escalas de D3 comparten la misma interfaz: .domain() para los datos y .range() para los valores visuales.
Conclusiones de la lección
En esta lección se ha construido un gráfico de barras completo con D3, paso a paso, desde los elementos más básicos hasta un resultado profesional.
Conceptos clave
Creación de elementos
- D3 crea elementos SVG con
d3.create()y.append() - El encadenamiento de métodos permite construir elementos de forma fluida
.node()extrae el elemento del DOM para que Observable Framework lo muestre
Data join
- El patrón
selectAll → data → joincrea un elemento por cada dato del array - Dentro del join, las funciones
(d, i) => ...acceden al dato y su índice - Se pueden hacer múltiples data joins en el mismo SVG (barras + etiquetas + valores)
Escalas
- Traducen valores de datos a valores de píxeles
d3.scaleLinear()para valores continuos,d3.scaleBand()para categorías.domain()define el rango de datos,.range()define el rango visual- Son el equivalente manual de lo que Plot calcula automáticamente
Ejes
d3.axisBottom()yd3.axisLeft()generan ejes completos a partir de una escala- Se aplican con
.call()sobre un grupo<g>posicionado con translate .nice()redondea el dominio,.ticks()controla el número de marcas
Patrón de márgenes
- El objeto
margin = {top, right, bottom, left}es convención estándar en D3 innerWidthyinnerHeightdefinen el área de dibujo real- Un grupo
<g>con translate aplica los márgenes a todo el contenido
Reflexión final
Construir un gráfico de barras en D3 requiere significativamente más código que en Observable Plot. Esto no es un defecto sino una consecuencia del control total que D3 ofrece: cada elemento, cada posición, cada atributo está bajo nuestro control directo.
La clave está en reconocer cuándo ese control adicional es necesario:
- Si el gráfico es estándar y se busca productividad → usar Plot
- Si se necesitan animaciones, interacciones complejas o layouts personalizados → usar D3
- En la mayoría de proyectos reales → combinar ambos dentro de Observable Framework
En la próxima lección exploraremos las capacidades de D3 que justifican su complejidad adicional: transiciones, interacciones avanzadas y visualizaciones que Plot no puede crear.
Próximo paso: En la siguiente lección, D3 en la práctica, exploraremos transiciones, interacciones avanzadas y la combinación de D3 con Plot para crear visualizaciones que aprovechen lo mejor de cada herramienta.