Anotaciones: text, label, annotate

r
visualizacion
ggplot2
Etiquetas, líneas de referencia y marcadores destacados. geom_text vs geom_label, ggrepel para evitar solapamientos, annotate() para marcadores fijos y líneas de referencia con hline/vline/abline.

¿Para qué anotar?

Un gráfico sin anotación es exploración. Un gráfico con anotación dirige al lector exactamente a lo que tiene que ver. Es la diferencia entre “aquí están los datos” y “este punto es el que importa”.

Las anotaciones cubren tres necesidades distintas:

  1. Etiquetar puntos concretos (un país en un scatter, una empresa en un ranking).
  2. Añadir texto fijo en una posición del gráfico (notas, etiquetas de zonas).
  3. Marcar valores de referencia (líneas verticales para fechas clave, horizontales para umbrales).

Cada una tiene su herramienta.

geom_text vs geom_label

Para etiquetar puntos basados en datos, hay dos geoms:

library(ggplot2)
library(palmerpenguins)

# Filtramos para evitar saturar
peng_sample <- na.omit(penguins)[1:20, ]

# geom_text: texto sin fondo
ggplot(peng_sample, aes(bill_length_mm, flipper_length_mm,
                       label = species)) +
  geom_point() +
  geom_text(nudge_y = 1, size = 3)

# geom_label: texto con fondo (recuadro)
ggplot(peng_sample, aes(bill_length_mm, flipper_length_mm,
                       label = species)) +
  geom_point() +
  geom_label(nudge_y = 1, size = 3)

Cuándo cada uno:

  • geom_text: texto puro. Útil cuando el fondo del gráfico es claro y limpio.
  • geom_label: texto con caja de fondo. Mejor cuando hay densidad de puntos y el texto se confundiría con los marcadores.

nudge_x / nudge_y desplazan ligeramente la etiqueta de la coordenada original para que no se superponga al punto.

El problema del solapamiento

En cuanto pasas de 5-10 etiquetas, todas se amontonan. Manualmente con nudge_* no escala. La solución estándar es ggrepel:

install.packages("ggrepel")
library(ggrepel)

ggplot(peng_sample, aes(bill_length_mm, flipper_length_mm,
                       label = species)) +
  geom_point() +
  geom_text_repel(size = 3, max.overlaps = 20)

ggrepel aplica un algoritmo de repulsión: las etiquetas se desplazan automáticamente para no solaparse entre sí ni con los puntos. Es el estándar moderno, adóptalo desde el principio.

Variantes:

  • geom_text_repel(): sin caja.
  • geom_label_repel(): con caja.
  • min.segment.length = 0 para que SIEMPRE aparezca la línea conectora.
  • max.overlaps = Inf si tienes muchas etiquetas y no quieres que ggrepel descarte algunas en silencio.

annotate(): marcadores fijos

Cuando quieres añadir texto, líneas o cualquier geom en una posición fija del gráfico (no basada en datos), usa annotate(). La diferencia con un geom_*() es:

  • geom_text() toma data y mapea filas a etiquetas.
  • annotate("text", ...) añade un único elemento, sin necesidad de un data frame.
ggplot(diamonds, aes(carat, price)) +
  geom_point(alpha = 0.05) +
  annotate(
    geom  = "text",
    x     = 2.5,
    y     = 18000,
    label = "Zona premium",
    color = "darkred",
    size  = 4
  )

annotate() acepta cualquier tipo de geom:

annotate("rect", xmin = 2, xmax = 3, ymin = 0, ymax = 20000,
         fill = "yellow", alpha = 0.2)
annotate("segment", x = 0, xend = 5, y = 10000, yend = 10000,
         color = "red", linetype = "dashed")

Casi cualquier patrón de “marcar una zona del gráfico” usa annotate().

Líneas de referencia: hline, vline, abline

Para líneas que cruzan todo el gráfico, hay tres geoms dedicados:

ggplot(diamonds, aes(carat, price)) +
  geom_point(alpha = 0.05) +
  geom_hline(yintercept = 10000, linetype = "dashed", color = "red") +
  geom_vline(xintercept = 2.0, linetype = "dotted", color = "blue") +
  geom_abline(slope = 5000, intercept = 0, color = "green")
  • geom_hline(yintercept = ...): línea horizontal en un valor de Y.
  • geom_vline(xintercept = ...): línea vertical en un valor de X.
  • geom_abline(slope, intercept): línea recta con pendiente e intercepto (útil para línea x=y, líneas de tendencia teóricas).

Casos típicos:

  • geom_hline(yintercept = 0) para resaltar el cero en gráficos de cambio.
  • geom_vline(xintercept = as.Date("2020-03-15")) para marcar el inicio de la pandemia en una serie temporal.
  • geom_abline(slope = 1, intercept = 0) para la diagonal x=y en gráficos de comparación predicho vs observado.

Con leyenda

Si quieres que la línea aparezca en la leyenda, mapea el color a un valor:

ggplot(diamonds, aes(carat, price)) +
  geom_point(alpha = 0.05) +
  geom_hline(aes(yintercept = 10000, color = "Umbral premium"),
             linetype = "dashed") +
  scale_color_manual(name = NULL, values = c("Umbral premium" = "red"))

Mapear color = "Umbral premium" dentro de aes crea una “variable” con un único nivel, que aparece en la leyenda con el color especificado.

Trampas habituales

  • geom_text con todos los puntos. Etiquetar 500 puntos no comunica nada, solo emborrona el gráfico. Filtra los puntos a etiquetar (top N, los relevantes) antes de pasarlos al geom: geom_text(data = filter(df, ranking <= 5), ...).
  • Olvidar que annotate("text", ...) no toma aes. Si escribes annotate("text", aes(...)), falla. annotate() espera valores literales en sus argumentos, no mapeos.
  • geom_hline(yintercept = mean(y)) que da error. La función mean() se evalúa fuera del contexto del data frame. Solución: calcula la media antes y pásala como valor: media_y <- mean(df$y); geom_hline(yintercept = media_y).
  • ggrepel que descarta etiquetas silenciosamente. Por defecto, si hay demasiadas etiquetas, ggrepel omite algunas y avisa con un warning. Si necesitas ver todas, sube max.overlaps = Inf o reduce las etiquetas pasadas.

En la siguiente entrega

Has aprendido a anotar. Con lo del bloque 2 (facetado, escalas, themes) y este tutorial, ya puedes producir gráficos con calidad de publicación. Quedan tres tutoriales del bloque 3, patchwork para combinar gráficos, color y accesibilidad y el caso completo final, que pulen los últimos detalles de presentación. El próximo es patchwork.