Manipulación

Aquí pasamos de solo ver el árbol a interactuar con él.
La manipulación nos permite explorar mejor la topología y preparar figuras más limpias:

👁️ Visualizar árbol con ggtree

## Paquetes
library(tidyverse)
library(ggtree)
library(treeio)
library(ape)

# Archivo ML (NEWICK)
f_raxml <- "../docs/raxml_cox1_Stenopelmatus.tre"

# Lectura con ape
ml_phylo <- read.tree(f_raxml)

# geom_tiplab() agregra nombres de terminales
# size controla el tamaño del texto
ml_p <- ggtree(ml_phylo) 

ml_p

📊 El “data” de un objeto phylo

Cuando cargamos un árbol en R con ape lo que tenemos es un objeto de clase phylo.
Este objeto contiene varias piezas internas:

# Estructura básica de un árbol phylo
str(ml_phylo)
List of 5
 $ edge       : int [1:192, 1:2] 98 99 100 101 101 100 102 102 103 104 ...
 $ edge.length: num [1:192] 0.05274 0.00861 0.06709 0.00582 0.00603 ...
 $ Nnode      : int 96
 $ node.label : chr [1:96] "" "99" "66" "100" ...
 $ tip.label  : chr [1:97] "CNIN3621" "CNIN3620" "CNIN3640" "CNIN3913" ...
 - attr(*, "class")= chr "phylo"
 - attr(*, "order")= chr "cladewise"
  • 🔀 edge → tabla de conexiones entre nodos (quién es hijo de quién).

  • 📏 edge.length → longitudes de las ramas.

  • 🌱 Nnode → número de nodos internos.

  • 🏷️ node.label → etiquetas de nodos (ej. valores de bootstrap o posterior).

  • 🌿 tip.label → nombres de las terminales.

Cuando usamos ggtree(...), el objeto se transforma en un data.frame interno accesible con $data.

#  Transforma en un data.frame
ml_df <- ml_p$data

# Ver las primeras 10 lineas
head(ml_df, 10)
# A tibble: 10 × 9
   parent  node branch.length label    isTip     x     y branch angle
    <int> <int>         <dbl> <chr>    <lgl> <dbl> <dbl>  <dbl> <dbl>
 1    101     1    0.00582    CNIN3621 TRUE  0.134     4  0.131  14.8
 2    101     2    0.00603    CNIN3620 TRUE  0.134     5  0.131  18.6
 3    102     3    0.0530     CNIN3640 TRUE  0.133     6  0.106  22.3
 4    105     4    0.00000100 CNIN3913 TRUE  0.135     8  0.135  29.7
 5    105     5    0.00000100 CNIN3641 TRUE  0.135     9  0.135  33.4
 6    104     6    0.00000100 CNIN4215 TRUE  0.135     7  0.135  26.0
 7    107     7    0.00430    CNIN4319 TRUE  0.156    11  0.154  40.8
 8    107     8    0.00000100 CNIN4216 TRUE  0.151    12  0.151  44.5
 9    106     9    0.00170    CNIN4217 TRUE  0.148    10  0.147  37.1
10    109    10    0.0443     EF030183 TRUE  0.190    13  0.168  48.2

Ese data incluye columnas como:

  • 🔗 parent → ID del nodo padre.

  • 🔢 node → # de nodo.

  • 📏 branch.length → Longitud de la rama.

  • 🏷️ label → Nombre del taxón (si es tip) o valor de soporte (si es nodo).

  • ✅/❌ isTip → TRUE si es terminal, FALSE si es nodo interno.

  • 📐 x, y → Coordenadas asignadas por ggtree para graficar.

  • 🔀 branch → Longitud usada para posicionar gráficamente.

  • 📐 angle → Ángulo de la rama (en layouts circulares o fan).

🔗 Integrar metadatos al árbol con el operador %<+% de ggtree

💾 Metadatos para el árbol (qué y para qué)

A veces necesitamos información extra para cada terminal (muestra) que no viene en el árbol:

  • Asignación taxonómica: especie, género, clado, área biogeográfica, etc.

  • Rasgos morfológicos: presencia/ausencia (0/1), conteos (número de espinas), medidas (mm), etc.

  • Datos ecológicos: hábitat, elevación, bioma, etc.

📌 Integrar metadatos al árbol con %<+%

Los metadatos se integran al gráfico con el operador %<+%, que funciona como un “enchufe”:

  • Fusiona por la columna label.

  • Esa columna debe estar en tu dataframe de metadatos y debe coincidir exactamente con los nombres de las terminales del árbol (ml_p$data$label).

👉 Es decir, el df con tus metadatos necesita una columna llamada label, donde cada valor sea idéntico al nombre de las puntas del árbol.

✅ Cuándo usar %<+%

  • Cuando tus IDs de tips en el árbol y tu tabla coinciden 1:1 (mismo texto).

  • Cuando quieres evitar hacer joins manuales con dplyr y trabajar de inmediato con geom_* de ggtree.

🧩 Requisitos mínimos

  • La tabla debe tener una columna con los mismos nombres que label del árbol.

  • Si tu columna no se llama label, renómbrala en tu tabla.

# Cargar la tabla
df_stenopelmatus <- read.csv("../docs/stenopelmatus.csv") %>%
    rename(label = Sample) # renombrar las columna Sample por label

head(df_stenopelmatus)
     label    Lat    Long                    Species                    Text
1 CNIN3620 15.649 -92.809 Stenopelmatus_micropterous 40: S. sp. aff. chiapas
2 CNIN3621 15.649 -92.809 Stenopelmatus_micropterous                    <NA>
3 CNIN3624 15.722 -92.939 Stenopelmatus_macropterous      25: S. sartorianus
4 CNIN3625 15.722 -92.939 Stenopelmatus_macropterous                    <NA>
5 CNIN3626 15.722 -92.939 Stenopelmatus_macropterous                    <NA>
6 CNIN3630 19.631 -97.009     Stenopelmatus_apterous                    <NA>
    Color Presencia_alas                                      Region
1   negro             no          Mexico: Chiapas Highlands province
2   negro             no          Mexico: Chiapas Highlands province
3 naranja             si          Mexico: Chiapas Highlands province
4 naranja             si          Mexico: Chiapas Highlands province
5 naranja             si          Mexico: Chiapas Highlands province
6    rojo             no Mexico: Transmexican Volcanic Belt province
#Unir árbol + informacion extra 
ml_p <- ml_p %<+% df_stenopelmatus

# Transforma en un data.frame
ml_df <- ml_p$data

head(ml_df[,4:12])
# A tibble: 6 × 9
  label    isTip     x     y branch angle   Lat  Long Species                   
  <chr>    <lgl> <dbl> <dbl>  <dbl> <dbl> <dbl> <dbl> <chr>                     
1 CNIN3621 TRUE  0.134     4  0.131  14.8  15.6 -92.8 Stenopelmatus_micropterous
2 CNIN3620 TRUE  0.134     5  0.131  18.6  15.6 -92.8 Stenopelmatus_micropterous
3 CNIN3640 TRUE  0.133     6  0.106  22.3  16.7 -94.2 Stenopelmatus_micropterous
4 CNIN3913 TRUE  0.135     8  0.135  29.7  16.2 -96.5 Stenopelmatus_micropterous
5 CNIN3641 TRUE  0.135     9  0.135  33.4  16.2 -96.5 Stenopelmatus_micropterous
6 CNIN4215 TRUE  0.135     7  0.135  26.0  16.2 -96.5 Stenopelmatus_micropterous

🔢 Ver números de nodos

# ggtree(...)$data expone la tabla interna con columnas como x, y, node, isTip, label
ggtree(ml_phylo) +
  geom_text2(aes(label = node),
             hjust = -0.1, size = 2.5, color = "gray25") 

👉 Tip: los números de nodos no aparecen en el Newick, son generados por R al leer el árbol.

  • Los tips siempre se numeran del 1 al número de taxa.

  • Los nodos internos siguen con números consecutivos.

🔄Rotar el árbol

¿Por qué rotar un árbol?

  • No cambia la topología: rotar solo invierte el orden de los dos subclados hijos de un nodo interno. Las relaciones evolutivas permanecen idénticas.

  • Mejora la legibilidad: puedes alinear clados de interés, reducir cruces de ramas y acomodar etiquetas para una figura más limpia.

  • Estética para publicación: ayuda a que comparaciones (p. ej., entre clados hermanos) queden “lado a lado” como quieres mostrar en el manuscrito.

Ejemplo

# Rotar en el nodo 116 
p_rotate_116 <- ggtree(ml_phylo) +
    ggtree::rotate(node = 116) 

# ggtree guarda el arbol sin rotar y el rotado en una lista
# Árbol sin rotar
p_rotate_116[[1]] + 
    geom_text2(aes(label = node),
             hjust = -0.1, size = 2.5, color = "gray25") 

# Árbol rotado
p_rotate_116[[2]] + 
    geom_text2(aes(label = node),
             hjust = -0.1, size = 2.5, color = "gray25")  

# Extraes la tabla resultante tras la rotación
rotated_df <- p_rotate_116[[2]]$data

# Ver las primeras 10 lineas
head(rotated_df, 10)
# A tibble: 10 × 9
   parent  node branch.length label    isTip     x     y branch angle
    <int> <int>         <dbl> <chr>    <lgl> <dbl> <dbl>  <dbl> <dbl>
 1    101     1    0.00582    CNIN3621 TRUE  0.134     4  0.131  14.8
 2    101     2    0.00603    CNIN3620 TRUE  0.134     5  0.131  18.6
 3    102     3    0.0530     CNIN3640 TRUE  0.133     6  0.106  22.3
 4    105     4    0.00000100 CNIN3913 TRUE  0.135     8  0.135  29.7
 5    105     5    0.00000100 CNIN3641 TRUE  0.135     9  0.135  33.4
 6    104     6    0.00000100 CNIN4215 TRUE  0.135     7  0.135  26.0
 7    107     7    0.00430    CNIN4319 TRUE  0.156    11  0.154  40.8
 8    107     8    0.00000100 CNIN4216 TRUE  0.151    12  0.151  44.5
 9    106     9    0.00170    CNIN4217 TRUE  0.148    10  0.147  37.1
10    109    10    0.0443     EF030183 TRUE  0.190    13  0.168  48.2

🖼️ Ajustar el lienzo: ejes, límites y márgenes

📐 Cómo funcionan los ejes en ggtree

  • Eje X (x) → representa la distancia acumulada desde la raíz.

    • Si tu árbol es de Máxima Verosimilitud o inferencia bayesiana, X suele ser sustituciones por sitio.

    • Si es ultramétrico (ej. BEAST), X puede interpretarse como tiempo.

  • Eje Y (y) → es un índice que asigna ggtree automáticamente a cada terminal, de arriba a abajo.

    • No es biológico, solo indica la posición vertical de cada taxón.
# Rango en X (distancia/tiempo)
# xr → te dice de dónde a dónde llega el árbol en horizontal.
xr <- range(ml_df$x, na.rm = TRUE)

# Rango en Y (posición de tips/nodos)
# yr → te da el número mínimo y máximo de posiciones verticales (básicamente cuántos tips).
yr <- range(ml_df$y, na.rm = TRUE)

# 5% de aire a izquierda/derecha
pad_left  <- 0.05 * diff(xr)
pad_right <- 0.05 * diff(xr)

 ggtree(ml_phylo) +
  # Eje X visible y ajustado
  scale_x_continuous(
    name   = "Distancia (sust./sitio)", # cambia el nombre si es tiempo
    breaks = pretty(xr, n = 6),
    limits = c(xr[1] - pad_left, xr[2] + pad_right),
    expand = c(0, 0)  # usamos limits, no expand
  ) +
  # Eje Y visible con ticks
  scale_y_continuous(
    name   = "Número de taxa",
    breaks = pretty(yr, n = 5),
    expand = expansion(mult = c(0.02, 0.04))
  ) + 
  # Mostrar ticks y líneas (ggtree los oculta por default)
  theme(
    axis.line.y  = element_line(),
    axis.ticks.y = element_line(),
    axis.text.y  = element_text(size = 8),
    axis.title.y = element_text(size = 10),
    axis.line.x  = element_line(),
    axis.ticks.x = element_line(),
    axis.text.x  = element_text(size = 8),
    axis.title.x = element_text(size = 10)
    ) 

👉 Para que las etiquetas no se corten, el árbol sea legible y la figura mantenga proporciones al exportar.

🔑 Pasos básicos:

  • 📐 Calcular rangos en x y y desde ggtree(tr)$data.

  • ➕ Agregar un poco de aire a la derecha (pad_x) para las etiquetas.

  • 📏 Usar scale_x_continuous y scale_y_continuous con expand=0 para fijar límites exacto

  • ✂️ coord_cartesian(clip="off") → deja que sobresalgan etiquetas largas.

  • ↔︎️ plot.margin → agrega margen extra si se cortan textos.

  • 📊 geom_treescale → coloca una barra de escala en coordenadas reales.

👉 En pocas líneas: controlas el espacio visible, evitas recortes y aseguras consistencia en tus figuras.

# obtener el numero de terminales
ntips_s <- ml_df %>%
    filter(isTip == "TRUE") %>%
    nrow()

# Aire extra en X (5% del rango) para no cortar etiquetas
pad_x <- 0.05 * diff(xr)

ml_p_adj <- ml_p +
  geom_treescale(y = -3.5, width = 0.02) + # escala sustituciones por sitio.
  scale_x_continuous(limits = c(xr[1] - pad_x, xr[2] + pad_x),
                     expand = c(0, 0)) +   # Limites exactos sin padding automático
  scale_y_continuous(limits = c(-5, ntips_s + 3),
                     expand = c(0, 0)) +
  coord_cartesian(clip = "off") +          # Permitir etiquetas largas fuera del panel
  theme(plot.margin = margin(5.5, 25, 5.5, 5.5)) # Más aire a la derecha

ml_p_adj

🛡️Soportes de nodos

En un árbol de Máxima Verosimilitud (ML), los nodos internos suelen llevar valores de bootstrap que indican qué tan confiable es cada agrupación:

  • 🔢 Primero podemos mostrar los valores en crudo (números en cada nodo).

  • ⚫ Después, para figuras más limpias, podemos resumir gráficamente los soportes de acuerdo con Alfaro et al. (2003):

    • Bootstrap > 80 → círculos negros (alta confianza).

    • Bootstrap 50–80 → círculos grises (confianza moderada).

    • Bootstrap < 50 → círculos blancos (confianza baja).

👉 Esto permite pasar de una visualización informativa (con números) a una visualización estética y clara.

A) Mostrar soportes “en crudo”

# Soportes tal cual vienen del árbol (RAxML suele guardarlos en node.label)
ml_p_adj  +
    geom_text2(aes(label = label, subset = !isTip & label != ""),
               size = 2.5, vjust = -0.3, hjust = 1.2)

B) Puntos según umbrales de bootstrap

  • > 80: círculos negros

  • 50–80: círculos grises

  • < 50: círculos blancos

# Puntos por umbral de bootstrap
ml_p_adj  <- ml_p_adj  +
  # Negro: >80
  geom_point2(aes(subset = !isTip & as.numeric(label) > 80),
              size = 2.5, shape = 16, color = "black") +
  # Gris: 50–80
  geom_point2(aes(subset = !isTip & as.numeric(label) >= 50 & as.numeric(label) <= 80),
              size = 2.5, shape = 16, color = "grey40") +
  # Blanco: <50
  geom_point2(aes(subset = !isTip & as.numeric(label) < 50),
              size = 2.5, shape = 21, fill = "white", color = "black")

ml_p_adj 

🎨 Etiquetas de las terminales

Hasta ahora solo hemos visto el árbol como estructura y hemos manipulado nodos o tamaños. En esta parte vamos a enriquecerlo con nuestros metadatos —que añadimos en el paso previo con el operador %<+% de ggtree— para poder representar información extra en el árbol.

  • 🔤 Pintar los nombres de las terminales por categorías (ej. S. apterous, S. micropterous…).
# Paleta por categoría funcional (ajústala si quieres)
pal <- c(
  "Stenopelmatus_apterous"     = "#56ae6c",
  "Stenopelmatus_micropterous" = "#a24f99",
  "Stenopelmatus_macropterous" = "#af953c",
  "Ammopelmatus_apterous"     = "#6971c9",
  "Outgroup"        = "#ba4a4f"
)

ml_p_adj +
    geom_tiplab(aes(color = Species), size = 2.5, offset = 0.003) +
    scale_color_manual(
    values = pal,
    na.value = "black",
    # orden explícito de los grupos
    breaks = c("Ammopelmatus_apterous",
               "Stenopelmatus_apterous", 
               "Stenopelmatus_macropterous",
               "Stenopelmatus_micropterous",
               "Outgroup"),
    labels = c(Ammopelmatus_apterous = expression(bolditalic("A. apterous")),
               Stenopelmatus_apterous = expression(bolditalic("S. apterous")),
               Stenopelmatus_macropterous = expression(bolditalic("S. macropterous")),
               Stenopelmatus_micropterous = expression(bolditalic("S. micropterous")),
               Outgroup = expression(bold("Outgroup")))) +
  guides(color = guide_legend(title = "Species",
                              override.aes = list(size = 4))) +
  theme(
    legend.position = c(0.25, 0.98),  # esquina superior derecha (98% ancho/alto)
    legend.justification = c("right", "top"), # anclar a esa esquina
    legend.background = element_rect(fill = "white", color = "black"), # recuadro
    legend.key = element_rect(fill = "white", color = "black"),        # caja de cada color
    legend.title = element_text(size = 10, face = "bold"),
    legend.text  = element_text(size = 9))

  • 🖼️ Resaltar clados completos con recuadros de color (highlight).
# pal: tu paleta por especie (ya definida antes)
ml_p_adj +
  geom_tiplab(
    aes(fill = Species, label = label),  # fill por especie
    color = "grey15",
    geom = "label",                      # cajita detrás del texto
    label.size = 0,                      # sin borde en la cajita
    label.padding = unit(0.12, "lines"), # relleno interno de la cajita
    size = 2.6,
    offset = 0.003,
    show.legend = TRUE
  ) +
  scale_fill_manual(
    values = pal,
    na.value = "black",
    # orden explícito de los grupos
    breaks = c("Ammopelmatus_apterous",
               "Stenopelmatus_apterous", 
               "Stenopelmatus_macropterous",
               "Stenopelmatus_micropterous",
               "Outgroup"),
    labels = c(Ammopelmatus_apterous = expression(bolditalic("A. apterous")),
               Stenopelmatus_apterous = expression(bolditalic("S. apterous")),
               Stenopelmatus_macropterous = expression(bolditalic("S. macropterous")),
               Stenopelmatus_micropterous = expression(bolditalic("S. micropterous")),
               Outgroup = expression(bold("Outgroup")))) +
  guides(fill = guide_legend(
    title = "Species",
    # símbolo cuadrado, sin letra y un poco más pequeño para que el gap sea menor
    override.aes = list(shape = 15, size = 4, label = NULL, colour = NA, stroke = 0),
    label.hjust = 0, title.hjust = 0)) +
  theme(
    legend.position      = c(0.02, 0.98),
    legend.justification = c("left", "top"),
    legend.background    = element_rect(fill = "white", color = "white"),
              # menos espacio entre key y texto
    legend.title         = element_text(size = 10, face = "bold"),
    legend.text          = element_text(size = 9, margin = margin(l = 0))
  )