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:


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

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?

El Y está invertido

Esta es la diferencia más importante respecto a lo que intuitivamente esperamos:

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?

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?

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?


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?


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?

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?

¿Por qué es importante?

En D3, los grupos se usan constantemente para:


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:

  1. Decidir las dimensiones: 500×320 para el SVG, reservar 100px para etiquetas
  2. 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)
  3. Calcular la posición Y de cada barra, etiqueta y valor, asegurando que estén alineados
  4. Repetir el código para cada país: 6 rectángulos, 6 etiquetas, 6 valores
  5. 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

Sistema de coordenadas

Elementos primitivos

Agrupación y posicionamiento

El costo de lo manual


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.

Bibliografía

Bostock, M., Ogievetsky, V., & Heer, J. (2011). D3: Data-Driven Documents. IEEE Transactions on Visualization and Computer Graphics, 17(12), 2301–2309. https://doi.org/10.1109/TVCG.2011.185
Munzner, T. (2015). Visualization analysis & design. CRC Press.