SVG: el lenguaje visual de D3
¿Por qué aprender SVG?
En las lecciones anteriores, Observable Plot se encargaba de crear los elementos visuales por nosotros: al escribir Plot.dot(), la biblioteca generaba internamente elementos <circle> dentro de un formato gráfico llamado SVG. Nunca tuvimos que pensar en esos detalles.
Con D3, eso cambia. D3 no dibuja gráficos automáticamente: es una biblioteca que nos da herramientas para manipular elementos SVG directamente. Esto significa más control, pero también requiere entender el medio donde se dibuja (Bostock et al., 2011).
Antes de usar D3, necesitamos comprender SVG. Necesitamos entender las reglas del espacio donde D3 trabaja.
En esta lección aprenderemos a:
- Entender qué es SVG y por qué D3 lo utiliza
- Manejar el sistema de coordenadas SVG (diferente al de Plot)
- Crear elementos primitivos: rectángulos, círculos, líneas y texto
- Aplicar estilos visuales que ya conocemos de Plot
- Agrupar y posicionar elementos con
<g>ytranslate - Construir un gráfico de barras a mano para entender lo que D3 automatizará
¿Qué es SVG?
SVG (Scalable Vector Graphics) es un formato de imagen basado en instrucciones matemáticas, no en píxeles. En lugar de guardar el color de cada punto (como una fotografía), SVG describe las formas: "dibuja un rectángulo de 100×50 en la posición (20, 30) con relleno azul".
Ventajas para visualización de datos:
- Escalable: se puede ampliar sin perder calidad (ideal para pantallas de diferentes tamaños)
- Manipulable: cada elemento es un objeto del DOM que se puede modificar con JavaScript
- Estilizable: acepta propiedades CSS como
fill,strokeyopacity - Interactivo: cada forma puede responder a eventos del mouse (click, hover, etc.)
Estas características hacen de SVG el medio ideal para gráficos interactivos, y por eso D3 lo eligió como su formato principal (Munzner, 2015).
El sistema de coordenadas SVG
Todo SVG comienza con un contenedor que define el espacio de dibujo:
Ver código SVG sistema de coordenadas
<svg width="400" height="200" style="border: 1px solid #ccc;">
<circle cx="0" cy="0" r="5" fill="#9B1C26" />
<circle cx="200" cy="100" r="5" fill="#9B1C26" />
<circle cx="400" cy="200" r="5" fill="#9B1C26" />
</svg>
¿Qué hace cada parte?
<svg width="400" height="200">: crea un espacio de dibujo de 400 píxeles de ancho por 200 de altocx="0" cy="0": posiciona el primer punto en la esquina superior izquierda (el origen)cx="200" cy="100": el centro exacto del SVGcx="400" cy="200": la esquina inferior derecha
El Y está invertido
Esta es la diferencia más importante respecto a lo que intuitivamente esperamos:
- X crece hacia la derecha (igual que en matemáticas)
- Y crece hacia abajo (opuesto a matemáticas)
En Observable Plot nunca notamos esto porque la biblioteca ajusta las coordenadas automáticamente. En SVG y D3, debemos recordar que y=0 es la parte superior y los valores crecen hacia abajo. Esto afecta directamente cómo posicionamos barras, texto y cualquier elemento.
Elementos primitivos
SVG ofrece un conjunto de formas básicas que se combinan para crear visualizaciones completas. Cada forma tiene atributos que definen su posición, tamaño y apariencia.
Rectángulo: <rect>
El rectángulo es la base de los gráficos de barras, histogramas, heatmaps y muchas otras visualizaciones.
Ver código SVG rectángulo
<svg width="400" height="120">
<rect x="20" y="20" width="200" height="80" fill="#9B1C26" />
</svg>
¿Qué hace cada parte?
- x="20": posición horizontal de la esquina superior izquierda del rectángulo
- y="20": posición vertical de la esquina superior izquierda
- width="200": ancho en píxeles
- height="80": alto en píxeles
- fill="#9B1C26": color de relleno
Un rectángulo se define por su esquina superior izquierda más su ancho y alto. Esto es importante al construir barras: la posición Y de la barra no es su base, sino su tope.
Círculo: <circle>
El círculo es la base de los scatter plots y gráficos de burbujas (equivalente a Plot.dot()).
Ver código SVG círculo
<svg width="400" height="120">
<circle cx="200" cy="60" r="40" fill="#9B1C26" />
</svg>
¿Qué hace cada parte?
- cx="200": posición horizontal del centro del círculo
- cy="60": posición vertical del centro
- r="40": radio en píxeles
A diferencia del rectángulo, el círculo se posiciona por su centro, no por una esquina. Esto es más intuitivo para scatter plots: cada punto se ubica exactamente en las coordenadas de sus datos.
Línea: <line>
La línea conecta dos puntos. Es la base de los gráficos de línea, ejes y reglas de referencia (equivalente a Plot.ruleY()).
Ver código SVG línea
<svg width="400" height="120">
<line x1="20" y1="100" x2="380" y2="20" stroke="#9B1C26" stroke-width="2" />
</svg>
¿Qué hace cada parte?
- x1, y1: coordenadas del punto de inicio
- x2, y2: coordenadas del punto de fin
- stroke="#9B1C26": color de la línea (las líneas no tienen
fill, solostroke) - stroke-width="2": grosor de la línea en píxeles
Texto: <text>
El elemento texto permite agregar etiquetas, títulos y valores numéricos directamente en el gráfico.
Ver código SVG texto
<svg width="400" height="80">
<text x="200" y="45" text-anchor="middle" font-size="18" fill="#333">
Expectativa de vida (años)
</text>
</svg>
¿Qué hace cada parte?
- x="200", y="45": posición del punto de anclaje del texto
- text-anchor="middle": alinea el texto centrado respecto al punto de anclaje (otras opciones:
"start","end") - font-size="18": tamaño de la fuente en píxeles
- fill="#333": el color del texto se define con
fill, no concolor
Atributos visuales
Los atributos de estilo en SVG son los mismos que usamos en Observable Plot. La diferencia es que en Plot los pasábamos como propiedades de JavaScript; en SVG son atributos XML directos.
Ver código SVG atributos visuales
<svg width="500" height="120">
<!-- Relleno sólido -->
<rect x="20" y="20" width="120" height="80" fill="#9B1C26" />
<!-- Relleno semitransparente -->
<rect
x="160"
y="20"
width="120"
height="80"
fill="#9B1C26"
fill-opacity="0.4"
/>
<!-- Solo borde, sin relleno -->
<rect
x="300"
y="20"
width="120"
height="80"
fill="none"
stroke="#9B1C26"
stroke-width="3"
/>
</svg>
Correspondencia con Observable Plot
| SVG | Observable Plot | Función |
|---|---|---|
| fill | fill | Color de relleno |
| fill-opacity | fillOpacity | Transparencia del relleno (0 a 1) |
| stroke | stroke | Color del borde |
| stroke-width | strokeWidth | Grosor del borde |
| stroke-opacity | strokeOpacity | Transparencia del borde |
| opacity | opacity | Transparencia general del elemento |
La única diferencia de sintaxis es que SVG usa guiones (fill-opacity, stroke-width) mientras que JavaScript usa camelCase (fillOpacity, strokeWidth). Los valores son idénticos.
El elemento <path>
El elemento <path> es el más versátil de SVG: puede dibujar cualquier forma mediante una serie de comandos contenidos en el atributo d (de data). Cada comando es una letra seguida de coordenadas.
Move to (M): mover sin dibujar
El comando M posiciona el cursor en un punto sin trazar nada. Es siempre el primer comando de cualquier path, equivale a levantar el lápiz y colocarlo en el punto de inicio.
Ver código SVG Move to
<svg width="400" height="120" style="border: 1px solid #eee;">
<!-- El punto rojo muestra dónde se posicionó el cursor -->
<circle cx="50" cy="80" r="4" fill="#9B1C26" />
<text x="50" y="70" text-anchor="middle" font-size="12" fill="#666">
M 50,80
</text>
<text x="200" y="60" text-anchor="middle" font-size="13" fill="#999">
(el cursor está en 50,80 pero no se dibujó nada)
</text>
</svg>
Line to (L): líneas rectas
El comando L dibuja una línea recta desde la posición actual del cursor hasta el punto indicado. Se pueden encadenar varios L para crear formas poligonales.
Ver código SVG Line to
<svg width="400" height="150">
<!-- Path con líneas rectas -->
<path
d="M 30,120 L 150,20 L 270,100 L 370,40"
fill="none"
stroke="#9B1C26"
stroke-width="2.5"
/>
<!-- Puntos en cada vértice -->
<circle cx="30" cy="120" r="4" fill="#9B1C26" />
<circle cx="150" cy="20" r="4" fill="#9B1C26" />
<circle cx="270" cy="100" r="4" fill="#9B1C26" />
<circle cx="370" cy="40" r="4" fill="#9B1C26" />
<!-- Etiquetas de cada comando -->
<text x="30" y="138" text-anchor="middle" font-size="11" fill="#666">
M 30,120
</text>
<text x="150" y="16" text-anchor="middle" font-size="11" fill="#666">
L 150,20
</text>
<text x="270" y="118" text-anchor="middle" font-size="11" fill="#666">
L 270,100
</text>
<text x="370" y="36" text-anchor="middle" font-size="11" fill="#666">
L 370,40
</text>
</svg>
El cursor se mueve a (30,120) con M, luego dibuja líneas rectas hasta cada punto con L. Esto es exactamente lo que un gráfico de líneas hace internamente: conectar valores con segmentos rectos.
Curve to (C): curvas Bézier
El comando C dibuja una curva suave entre dos puntos, controlada por dos puntos de control que determinan la dirección y curvatura. Es el comando que D3 usa para generar líneas suavizadas y áreas con curve: "basis".
Ver código SVG Curve to
<svg width="400" height="180">
<!-- Curva Bézier cúbica -->
<path
d="M 30,150 C 100,10 300,10 370,150"
fill="none"
stroke="#9B1C26"
stroke-width="2.5"
/>
<!-- Punto de inicio y fin (sólidos) -->
<circle cx="30" cy="150" r="5" fill="#9B1C26" />
<circle cx="370" cy="150" r="5" fill="#9B1C26" />
<!-- Puntos de control (vacíos) -->
<circle cx="100" cy="10" r="4" fill="none" stroke="#999" stroke-width="1.5" />
<circle cx="300" cy="10" r="4" fill="none" stroke="#999" stroke-width="1.5" />
<!-- Líneas guía hacia los puntos de control -->
<line
x1="30"
y1="150"
x2="100"
y2="10"
stroke="#ccc"
stroke-width="1"
stroke-dasharray="4"
/>
<line
x1="370"
y1="150"
x2="300"
y2="10"
stroke="#ccc"
stroke-width="1"
stroke-dasharray="4"
/>
<!-- Etiquetas -->
<text x="30" y="170" text-anchor="middle" font-size="11" fill="#666">
inicio (30,150)
</text>
<text x="370" y="170" text-anchor="middle" font-size="11" fill="#666">
fin (370,150)
</text>
<text x="100" y="8" text-anchor="middle" font-size="11" fill="#999">
control 1
</text>
<text x="300" y="8" text-anchor="middle" font-size="11" fill="#999">
control 2
</text>
</svg>
¿Cómo leer C 100,10 300,10 370,150?
- (100,10): primer punto de control — "tira" la curva hacia arriba a la izquierda
- (300,10): segundo punto de control — "tira" la curva hacia arriba a la derecha
- (370,150): punto de destino donde termina la curva
Los puntos de control no están sobre la curva: la atraen como imanes. Las líneas punteadas en el ejemplo muestran esta relación. Moviendo los puntos de control se cambia la forma de la curva sin cambiar el inicio ni el fin.
Combinando comandos
Un path puede mezclar líneas rectas, curvas y cierres en una sola secuencia:
Ver código SVG combinando comandos
<svg width="400" height="180">
<!-- Secuencia: Move → Line → Curve → Line -->
<path
d="M 30,150 L 120,30 C 180,30 220,150 280,80 L 370,80"
fill="none"
stroke="#9B1C26"
stroke-width="2.5"
/>
<!-- Puntos en cada vértice -->
<circle cx="30" cy="150" r="4" fill="#9B1C26" />
<circle cx="120" cy="30" r="4" fill="#9B1C26" />
<circle cx="280" cy="80" r="4" fill="#9B1C26" />
<circle cx="370" cy="80" r="4" fill="#9B1C26" />
<!-- Anotaciones -->
<text
x="75"
y="105"
font-size="11"
fill="#666"
transform="rotate(-50, 75, 105)"
>
L (recta)
</text>
<text x="200" y="55" font-size="11" fill="#666">C (curva)</text>
<text x="325" y="73" font-size="11" fill="#666">L (recta)</text>
</svg>
Resumen de comandos
| Comando | Significado | Parámetros | Ejemplo |
|---|---|---|---|
| M x,y | Move to — posiciona el cursor sin dibujar | Punto de destino | M 30,150 |
| L x,y | Line to — línea recta hasta el punto | Punto de destino | L 120,30 |
| C x1,y1 x2,y2 x,y | Curve to — curva Bézier cúbica | 2 controles + destino | C 180,30 220,150 280,80 |
| Z | Close — cierra el camino con línea recta al inicio | Ninguno | Z |
En la práctica, no escribiremos paths a mano. D3 tiene generadores como d3.line() y d3.arc() que crean estos comandos automáticamente a partir de datos. Pero es importante saber que cada línea de un gráfico de línea, cada arco de un pie chart y cada frontera de un mapa coroplético son, en el fondo, un atributo d de un elemento <path>.
Agrupación con <g> y translate
El elemento <g> (group) agrupa varios elementos SVG y permite moverlos, escalarlos o rotarlos como una unidad. La transformación más usada es translate(x, y), que desplaza todo el grupo a una nueva posición.
Ver código SVG agrupación
<svg width="400" height="180">
<!-- Primer grupo: desplazado a (30, 30) -->
<g transform="translate(30, 30)">
<rect width="100" height="60" fill="#9B1C26" />
<text x="50" y="35" text-anchor="middle" fill="white" font-size="14">
Chile
</text>
</g>
<!-- Segundo grupo: desplazado a (30, 110) -->
<g transform="translate(30, 110)">
<rect width="80" height="60" fill="#9B1C26" fill-opacity="0.7" />
<text x="40" y="35" text-anchor="middle" fill="white" font-size="14">
Perú
</text>
</g>
</svg>
¿Qué hace cada parte?
<g transform="translate(30, 30)">: mueve todo el grupo 30 píxeles a la derecha y 30 hacia abajo- Los elementos dentro del grupo usan coordenadas relativas al grupo, no al SVG completo
<rect width="100" height="60"/>: sinxniy, el rectángulo se dibuja en (0,0) del grupo, que gracias al translate es (30,30) del SVG
¿Por qué es importante?
En D3, los grupos se usan constantemente para:
- Crear márgenes: un grupo con
translate(margin.left, margin.top)desplaza todo el contenido del gráfico - Posicionar ejes: cada eje va en su propio
<g>con un translate adecuado - Agrupar elementos repetidos: cada barra con su etiqueta puede ser un grupo que se posiciona como unidad
Construyendo un gráfico de barras a mano
Con lo que sabemos hasta ahora, podemos construir un gráfico de barras completo sin D3, usando solo SVG puro. El objetivo es experimentar lo laborioso que resulta calcular posiciones y tamaños manualmente, para luego apreciar lo que D3 automatiza.
Representaremos la expectativa de vida de 6 países de Sudamérica:
Ver código SVG gráfico de barras manual
<svg width="500" height="320">
<!-- Título -->
<text
x="250"
y="20"
text-anchor="middle"
font-size="16"
font-weight="bold"
fill="#333"
>
Expectativa de vida en Sudamérica
</text>
<!-- Área del gráfico (desplazada para dejar espacio a etiquetas) -->
<g transform="translate(100, 40)">
<!-- Etiquetas del eje Y (países) -->
<text x="-10" y="22" text-anchor="end" font-size="13" fill="#333">
Chile
</text>
<text x="-10" y="62" text-anchor="end" font-size="13" fill="#333">
Uruguay
</text>
<text x="-10" y="102" text-anchor="end" font-size="13" fill="#333">
Colombia
</text>
<text x="-10" y="142" text-anchor="end" font-size="13" fill="#333">
Ecuador
</text>
<text x="-10" y="182" text-anchor="end" font-size="13" fill="#333">
Argentina
</text>
<text x="-10" y="222" text-anchor="end" font-size="13" fill="#333">
Perú
</text>
<!-- Barras (anchos calculados: valor / 85 * 350) -->
<rect
x="0"
y="5"
width="329"
height="30"
fill="#9B1C26"
fill-opacity="0.85"
/>
<rect
x="0"
y="45"
width="320"
height="30"
fill="#9B1C26"
fill-opacity="0.85"
/>
<rect
x="0"
y="85"
width="317"
height="30"
fill="#9B1C26"
fill-opacity="0.85"
/>
<rect
x="0"
y="125"
width="316"
height="30"
fill="#9B1C26"
fill-opacity="0.85"
/>
<rect
x="0"
y="165"
width="315"
height="30"
fill="#9B1C26"
fill-opacity="0.85"
/>
<rect
x="0"
y="205"
width="315"
height="30"
fill="#9B1C26"
fill-opacity="0.85"
/>
<!-- Valores al final de cada barra -->
<text x="335" y="25" font-size="12" fill="#333">80.0</text>
<text x="326" y="65" font-size="12" fill="#333">77.8</text>
<text x="323" y="105" font-size="12" fill="#333">77.1</text>
<text x="322" y="145" font-size="12" fill="#333">76.8</text>
<text x="321" y="185" font-size="12" fill="#333">76.5</text>
<text x="321" y="225" font-size="12" fill="#333">76.5</text>
<!-- Línea base del eje X -->
<line x1="0" y1="0" x2="0" y2="240" stroke="#ccc" stroke-width="1" />
</g>
</svg>
¿Qué tuvimos que hacer manualmente?
Para construir este gráfico simple fue necesario:
- Decidir las dimensiones: 500×320 para el SVG, reservar 100px para etiquetas
- Calcular el ancho de cada barra: convertir cada valor (76.5–80.0) a píxeles con una regla de tres (
valor / máximo * anchoDisponible) - Calcular la posición Y de cada barra, etiqueta y valor, asegurando que estén alineados
- Repetir el código para cada país: 6 rectángulos, 6 etiquetas, 6 valores
- Ajustar manualmente la posición X de los valores para que no se encimen con las barras
Comparación con Observable Plot
El mismo gráfico en Observable Plot se escribe así:
Ver código Plot.plot equivalente
Plot.plot({
marginLeft: 100,
marks: [
Plot.barX(
datos
.filter((d) => d.continent === "South America")
.sort((a, b) => b.life_expectancy - a.life_expectancy)
.slice(0, 6),
{
y: "country",
x: "life_expectancy",
fill: "#9B1C26",
fillOpacity: 0.85,
sort: { y: "-x" },
}
),
Plot.ruleX([0]),
],
})
¿Qué hace Plot que nosotros hicimos a mano?
| Tarea | SVG manual | Observable Plot |
|---|---|---|
| Calcular escala de valores a píxeles | Regla de tres manual | Automático |
| Posicionar cada barra | Coordenadas Y hardcodeadas | Automático |
| Generar ejes con etiquetas | Elementos <text> uno por uno |
Automático |
| Adaptar a diferentes datos | Reescribir todo el SVG | Cambiar el filtro |
| Ordenar las barras | Pre-ordenar datos y ajustar posiciones | sort: {y: "-x"} |
Esta es la diferencia fundamental entre manual y declarativo. Plot abstrae todo el trabajo repetitivo. Pero esa abstracción tiene un costo: cuando necesitamos algo que Plot no ofrece (animaciones, layouts personalizados, interacciones complejas), estamos limitados.
D3 ocupa el punto medio: automatiza los cálculos (escalas, ejes, posiciones) pero nos deja control total sobre los elementos SVG. En la próxima lección veremos cómo D3 genera el mismo gráfico de barras a partir de datos, eliminando el trabajo manual sin perder el control.
Resumen de elementos SVG
| Elemento | Atributos principales | Equivalente en Plot |
|---|---|---|
<svg> |
width, height |
Contenedor creado por Plot.plot() |
<rect> |
x, y, width, height |
Plot.barX(), Plot.barY(), Plot.rectY() |
<circle> |
cx, cy, r |
Plot.dot() |
<line> |
x1, y1, x2, y2 |
Plot.ruleX(), Plot.ruleY(), Plot.link() |
<text> |
x, y, text-anchor, font-size |
Plot.text(), title, subtitle |
<path> |
d (comandos M, L, C, Z) |
Plot.line(), Plot.areaY(), Plot.geo() |
<g> |
transform |
Márgenes y agrupación interna de Plot |
Resumen de atributos de estilo
| Atributo SVG | JavaScript (D3/Plot) | Valores comunes |
|---|---|---|
| fill | fill | Colores: "#9B1C26", "steelblue", "none" |
| fill-opacity | fillOpacity | 0 (transparente) a 1 (opaco) |
| stroke | stroke | Colores o "none" |
| stroke-width | strokeWidth | Píxeles: 1, 2, 0.5 |
| stroke-opacity | strokeOpacity | 0 a 1 |
| font-size | fontSize | Píxeles: 12, 14, 16 |
| text-anchor | textAnchor | "start", "middle", "end" |
| transform | transform | "translate(x,y)", "rotate(deg)" |
Regla general: SVG usa guiones (fill-opacity), JavaScript usa camelCase (fillOpacity). Los valores son los mismos.
Conclusiones de la lección
En esta lección se ha explorado SVG como el lenguaje visual fundamental sobre el que D3 construye sus visualizaciones.
Conceptos clave
SVG como medio de dibujo
- Formato vectorial escalable, manipulable e interactivo
- Cada forma es un elemento del DOM que JavaScript puede crear y modificar
- Es el mismo formato que Observable Plot usa internamente
Sistema de coordenadas
- Origen en la esquina superior izquierda
- X crece hacia la derecha, Y crece hacia abajo
- Diferente a la convención matemática; hay que invertir mentalmente el eje vertical
Elementos primitivos
<rect>se posiciona por su esquina superior izquierda<circle>se posiciona por su centro<line>conecta dos puntos constroke(no tienefill)<text>se alinea context-anchory se colorea confill<path>puede dibujar cualquier forma con comandos (M,L,C,Z)
Agrupación y posicionamiento
<g>agrupa elementos y aplica transformaciones colectivastranslate(x, y)desplaza todo el grupo- Los elementos dentro del grupo usan coordenadas relativas
El costo de lo manual
- Construir un gráfico a mano requiere calcular cada posición y tamaño
- Cualquier cambio en los datos obliga a recalcular todo
- D3 automatiza estos cálculos manteniendo el control sobre los elementos SVG
Reflexión final
SVG es un lenguaje sorprendentemente simple: con solo seis elementos primitivos se pueden construir visualizaciones completas. La dificultad no está en SVG mismo, sino en calcular las posiciones y tamaños correctos a partir de los datos. Precisamente eso es lo que D3 resuelve.
Al construir el gráfico de barras a mano, experimentamos directamente el problema que D3 soluciona: traducir valores de datos (76.5 años de vida) a valores de píxeles (315px de ancho) de forma consistente y automatizada. Las escalas de D3 son la respuesta a ese problema, y serán el tema central de la próxima lección.
Próximo paso: En la siguiente lección, Introducción a D3, aprenderemos a usar D3 para generar elementos SVG a partir de datos, automatizando todo lo que en esta lección hicimos manualmente.