How to make Co-phylogeny plot: easy tanglegram in R (Updated Method)

in

— 3,939 reads


Tanglegram is a representation of co-phylogeny where tips of two phylogenetic trees are linked. This method is super useful to visualize common traits shared by both trees. For example, it can be used to visualize host-pathogen (or host-symbiotic) evolution and see if there is any phylogenetic concordance between the two phylogenetic trees.

I was in need to visualize co-phylogeny of phylogenetic tree reconstructed from chromosomal and symbiotic genes. Surprisingly, I didn’t find any straight-forward solution in R that can be used for drawing tanglegram. Particularly I wanted to leverage the beautiful ggtree library. After trying out several methods, I found the following approach works well for me so far. I have released a small R package on it, which can be found on GitHub.

How to make a Tanglegram from scratch

Since my original post on creating co-phylogeny (tanglegram) plots in R, I’ve received a lot of great feedback and questions. Many of you pointed out a common headache: when connecting two trees, the lines often cross over each other into a tangled mess, making it hard to see true concordance. Others asked how to stop the connecting lines from striking through the tip labels. And why stops at visualizing two trees? Why not three or more?

এই সেপ্টেম্বর Oxford BioDiscovery-তে আমার Metagenomics and Amplicon Sequence Analysis লাইভ অনলাইন কোর্স শুরু হবে। বিস্তারিত জানতে ক্লিক করুন।

Here is how to build a clean, customizable tanglegram from scratch.

Let’s load some necessary libraries in R

library(ggplot2)
library(ggtree)
library(phangorn)
library(dplyr)
library(ggnewscale) # Required for new_scale_color()
library(ape)        # Required for generating random trees

I have been using and improving the core function for plotting tanglegrams for my research for a while. I’m sharing an updated, highly customizable method using base ggtree and ggplot2. This new approach includes a pre.rotate function that automatically flips nodes to minimize line crossing, and a lab_padargument to neatly offset the connecting lines from your labels.

Here are the two powerhouse functions for our new tanglegram workflow. You can load these directly into your R environment.

Function 1: pre.rotate() This function takes two phylogenetic trees and uses phytools::cophylo to rotate their internal nodes. This aligns the tips of both trees as closely as possible, dramatically reducing the “tangle” in the final plot.

pre.rotate <- function(tree1, tree2) {
  cophylo <- phytools::cophylo(tree1, tree2)
  rotated_tree1 <- cophylo[[1]][[1]]
  rotated_tree2 <- cophylo[[1]][[2]]
  return(list(rotated_tree1, rotated_tree2))
}

Function 2: common.tanglegram() This is the upgraded plotting function. It handles the alignment, tip connections, and coloring all in one go. Notice the added lab_pad parameter. This lets you add space between the tip and where the connecting line starts, so lines no longer cross through your text or tip points!

common.tanglegram <- function(tree1, tree2, column, sampletypecolors=NA,
                              t2_pad = 0.5, t2_y_scale = 1, t2_y_pos = 0,
                              lab_pad = 0.05, tiplab = FALSE, t2_tiplab_size = 3,
                              t2_tiplab_pad = 0) {
  
  # Extract tree data
  d1 <- tree1$data
  d2 <- tree2$data
  
  # Update the associated variable
  d1$tree <- 't1'
  d2$tree <- 't2'
  
  # Logic for rotating tree 2:
  # 1. (max(d2$x) - d2$x) perfectly flips the tree so tips face left.
  # 2. + max(d1$x) places it immediately to the right of Tree 1.
  # 3. + t2_pad adds the horizontal gap between them.
  d2$x <- (max(d2$x) - d2$x) + max(d1$x) + t2_pad
  d2$y <- d2$y * t2_y_scale
  d2$y <- d2$y + t2_y_pos
  
  
  # Draw cophylogeny
  pp <- tree1 + geom_tree(data=d2, layout = "dendrogram") + 
    geom_tippoint(data = d2, aes(x = x-0.005, y = y, color=ani.spp)) +
    geom_treescale(x=0.1, y=10)
  
  # Merge tree data for tips only
  combined_data <- rbind(d1, d2) %>% filter(isTip == TRUE)
  
  # Create lines connecting the tips and assign padding
  combined_data <- combined_data %>%
    group_by(label) %>%
    mutate(
      lab_x = case_when(
        tree == "t1" ~ x + lab_pad,
        tree == "t2" ~ x - lab_pad,
        TRUE ~ x
      )
    ) %>%
    ungroup()
  
  # Add connecting lines colored by the trait category
  pp <- pp +
    new_scale_color() +
    geom_line(
      aes(
        x = lab_x,
        y = y,
        group = label,
        color = .data[[column]]
      ),
      data = combined_data, 
      alpha = 0.4
    )
  
  # Apply custom or default colors
  if (missing(sampletypecolors) || is.null(sampletypecolors)) {
    pp <- pp + scale_color_viridis_d(option="turbo")   
  } else {
    pp <- pp + scale_color_manual(values = sampletypecolors) 
  }
  
  # Optionally show tip labels for tree 2
  if (tiplab) {
    pp <- pp +
      ggtree::geom_tiplab(
        aes(x = x - t2_tiplab_pad),
        size = t2_tiplab_size,
        data = d2,
        hjust = 1
      )
  }
  
  return(pp)
}

Now, showtime! Let’s generate some dummy data to visualize the tanglegram.

set.seed(42)
t1 <- phangorn::midpoint(ape::rtree(20))
t2 <- phangorn::midpoint(ape::rtree(20))

# Ensure the tip labels match between both trees
t2$tip.label <- t1$tip.label

# Create a dummy metadata frame matching the tip labels
meta <- data.frame(
  label = t1$tip.label,
  ani.spp = as.character(sample(1:5, 20, replace = TRUE)),
  plasmid.type = sample(c("Type_A", "Type_B", "Type_C"), 20, replace = TRUE)
)

These trees look like this:

Meta look like the following:

> head(meta)
  label ani.spp plasmid.type
1   t12       2       Type_A
2    t2       4       Type_A
3   t14       2       Type_C
4    t3       3       Type_B
5    t1       2       Type_B
6   t10       1       Type_C

Time to rotate the trees, define colors because we love customization, annotate the trees, and draw the final tanglegram!

rotated_trees <- pre.rotate(t1, t2)

t1 <- rotated_trees[[1]]
t2 <- rotated_trees[[2]]

# Define some colors for the tree points based on our dummy Species (1-5)
species_colors <- c("1"="#E69F00", "2"="#56B4E9", "3"="#009E73", "4"="#F0E442", "5"="#0072B2")

# Define colors for the connecting lines based on our dummy Traits
trait_colors <- c(
  "Type_A" = "purple", 
  "Type_B" = "forestgreen",
  "Type_C" = "dodgerblue"
)


# Annotate Tree 1 
tree1 <- ggtree(t1, ladderize=FALSE) %<+% meta + 
  geom_tippoint(aes(x = x + 0.05, color = ani.spp)) +
  scale_color_manual(values = species_colors, name = "Species Group") 

# Annotate Tree 2 
tree2 <- ggtree(t2, ladderize=FALSE) %<+% meta


# Draw the final tanglegram
common.tanglegram(tree1, tree2, column = "plasmid.type", 
                  lab_pad = 0.05, t2_tiplab_size = 3, 
                  t2_y_scale = 1, t2_y_pos = 0, t2_tiplab_pad = 0.5) +
  scale_color_manual(values = trait_colors, name = "Trait Type")

And voilà!

Plotting three trees in the tanglegram

To add a third tree (or fourth, or n-th) into the mix, we can expand our logic. ggplot2 and geom_line make this surprisingly elegant. Because geom_line() automatically connects points from left to right based on their x-coordinates, all we have to do is place Tree 2 to the right of Tree 1, and Tree 3 to the right of Tree 2.

Here is an updated function, triple.tanglegram(), designed to handle three trees. It will leave Tree 1 facing right, flip Tree 2 (facing left), and flip Tree 3 (facing left, positioned furthest to the right).

triple.tanglegram <- function(tree1, tree2, tree3, column, sampletypecolors=NA,
                              t2_pad = 0.5, t3_pad = 0.5, 
                              t2_y_scale = 1, t2_y_pos = 0,
                              t3_y_scale = 1, t3_y_pos = 0,
                              lab_pad = 0.05) {
  
  # Extract tree data
  d1 <- tree1$data
  d2 <- tree2$data
  d3 <- tree3$data
  
  # Update the associated variable
  d1$tree <- 't1'
  d2$tree <- 't2'
  d3$tree <- 't3'
  
  # Position Tree 2: Flipped and placed to the right of Tree 1
  d2$x <- (max(d2$x) - d2$x) + max(d1$x) + t2_pad
  d2$y <- d2$y * t2_y_scale + t2_y_pos
  
  # Position Tree 3: Flipped and placed to the right of Tree 2
  d3$x <- (max(d3$x) - d3$x) + max(d2$x) + t3_pad
  d3$y <- d3$y * t3_y_scale + t3_y_pos
  
  # Draw the base trees
  pp <- tree1 + 
    geom_tree(data=d2, layout = "dendrogram") + 
    geom_tippoint(data=d2, aes(x = x - 0.005, y = y, color=ani.spp)) +
    geom_tree(data=d3, layout = "dendrogram") +
    geom_tippoint(data=d3, aes(x = x - 0.005, y = y, color=ani.spp))
  
  # Merge tree data for tips only
  combined_data <- rbind(d1, d2, d3) %>% filter(isTip == TRUE)
  
  # Assign padding for the connecting lines
  combined_data <- combined_data %>%
    group_by(label) %>%
    mutate(
      lab_x = case_when(
        tree == "t1" ~ x + lab_pad,
        tree %in% c("t2", "t3") ~ x - lab_pad, # Both t2 and t3 face left, so lines start to their left
        TRUE ~ x
      )
    ) %>%
    ungroup()
  
  # Add connecting lines colored by the trait category
  # geom_line automatically connects t1 -> t2 -> t3 based on the x-coordinates
  pp <- pp +
    new_scale_color() +
    geom_line(
      aes(
        x = lab_x,
        y = y,
        group = label,
        color = .data[[column]]
      ),
      data = combined_data, 
      alpha = 0.4
    )
  
  # Apply custom or default colors
  if (missing(sampletypecolors) || is.null(sampletypecolors)) {
    pp <- pp + scale_color_viridis_d(option="turbo")   
  } else {
    pp <- pp + scale_color_manual(values = sampletypecolors) 
  }
  
  return(pp)
}

Let’s do a toy example:

# Random midpoint-rooted trees
t1 <- phangorn::midpoint(ape::rtree(20))
t2 <- phangorn::midpoint(ape::rtree(20))
t3 <- phangorn::midpoint(ape::rtree(20))

# Create base ggtree objects using previously defined meta
tree1 <- ggtree(t1, ladderize=FALSE) %<+% meta + 
  geom_tippoint(aes(x = x + 0.05, color = ani.spp)) +
  geom_treescale()

tree2 <- ggtree(t2, ladderize=FALSE) %<+% meta + geom_treescale()

tree3 <- ggtree(t3, ladderize=FALSE) %<+% meta + geom_treescale()

# Plot all three
triple.tanglegram(tree1, tree2, tree3, column = "pTi.Type", 
                  t2_pad = 1, t3_pad = 1,  # Control spacing between the trees
                  lab_pad = 0.05) +            # Keep lines from striking the nodes
  scale_color_manual(values = trait_colors)

Notice we cannot use pre.rotate function here, because phytools::cophylo is designed to minimize crossing between two trees. You can try sequentially rotating them (e.g., align t1 and t2, then use the rotated t2 to align t3)

Tangler: The R package

Due to the popularity of this tutorial, I have released a small R package that can help you to draw simple tanglegram from two ggtree objects.

The R package called TangleR, currently released in GitHub.

You can download it in R using the following command:

Here’s how to use this TangleR package:

Now let’s say you want to reorder the tips of both phylogeny so that the tips are better aligned, however the overall tree toplogy is unchanged. For that you can use the pre.rotate function. The only difference is, you need to make sure that you are using ladderize=FALSE when converting the rotated tree in a ggtree object, otherwise ggtree will override tip-order.

# Rotate the internal nodes so that tips of both trees are aligned
rotated_trees <- pre.rotate(t1, t2)

t1 <- rotated_trees[[1]]
t2 <- rotated_trees[[2]]

# Annotate Trees, make sure to set ladderize=F
tree1 <- ggtree(t1, ladderize=F)   %<+% meta +
  geom_tiplab() +
  geom_tippoint(aes(color=Genotype))

# Annotate Tree 2
tree2 <- ggtree(t2, ladderize=F) %<+% meta + geom_tiplab()


# Tanglegram, no line color
simple.tanglegram(tree1, tree2, Genotype, l_color = 'green3', Green, t2_pad = 0.3,
                           tiplab = T, lab_pad = 0.1, x_hjust = 1, t2_tiplab_size = 3)

You can also draw tanglegram for all traits in the column using common.tanglegram function.

common.tanglegram(tree1, tree2, column = 'Genotype', sampletypecolors = c('green4', 'red'), t2_pad = 0.3,
                           tiplab = T, lab_pad = 0.1, t2_tiplab_size = 3)

Discover more from Arafat Rahman

Subscribe to get the latest posts sent to your email.


Comments

26 responses to “How to make Co-phylogeny plot: easy tanglegram in R (Updated Method)”

  1. Jaume Salgado Bolarin Avatar
    Jaume Salgado Bolarin

    Hello,

    This tutorial has been a lifesaver as a molecular biologist who is dipping his toes into bioinformatics.

    However I wanted to ask: is there a way to do the “pre.rotate” on the second method (ggtree instead of tangleR).

    I have a nice tanglegram, but most of the lines cross over eachother, so seeing if there is a specific pattern can be difficult, so if there was a way so the tip order was “reorganised” automatically in R -while keeping the same tree- so that tips from left side were as aligned as possible with the tips with the same label on the right side, that would be great.

    Best regards, and again, thank you for the great tutorial

    1. Arafat Avatar

      Thank you so much. I think we can add a pre.rotate in the tutorial. I realize, this tutorial is more popular than the package tangleR. So I’ll update the tutorial soon to fix your issue.

      1. Arafat Avatar

        Updated!

  2. Tapanut Songkasupa Avatar
    Tapanut Songkasupa

    Thank you very much. Is it possible to compare more than 2 trees?

    1. Arafat Avatar

      Yes, I think so. I’ll update the tutorial to include 2+ trees. Thank you for commenting!

      1. Arafat Avatar

        Updated!

  3. Hi, thank you for this great article, there’s no easier way to create a tanglegram other than yours ! I am trying to use the new TangleR package and the example on this page doesn’t work, I’m getting this error:

    Error in unique.default(x, nmax = nmax) :
    unique() applies only to vectors

    Any idea where it could come from ?
    Thanks !
    Eric

    1. Arafat Avatar

      Thank you so much for reporting the issue. There was a small bug in the simple.tanglegram function, which has been corrected. Please test it again, and let me know if it has been fixed! Best, Arafat!

  4. Jianshu Avatar

    Hi, in my case, I have several same names (tip label) in one tree, and the name is also in the second tree, I want to add link from all such names in tree 1 to tree 2. In the code provided, link within tree 1 are created (they are the same tip label), 2 same names in tree 1, only 1 are linked to tree 2, another linked each other in tree1. How should I adjust the code to have what I want?

    1. Arafat Avatar

      Hello! The strain names has to be unique in ggtree. But, you can add a second column in your meta where you can have the same names in the tip labels. You can use the geom_tiplab2 to show this column info. Then use the new column to connect the lines. This might work, please try!

  5. Rehemah Gwokyalya Avatar
    Rehemah Gwokyalya

    Hi, this is really great. Thanks for sharing it with us. I ma wondering if it is possible to subset basing on what is present in both data sets i.e., x > 0 in both A and B data sets?

    1. Hi, thanks for commenting. If I understand your question correctly, you can make a new column based on a condition of interest, and use the new column to subset and plot connected lines.

  6. Manuela Avatar

    Hi! Is there a way to rotate nodes on the trees? I rotate them before adding the meta data to the tree file but I keep getting the same tree order.

    Also, is there also a way to make the lines different colors, like make each clade a different color?

    Thank you

    1. Arafat Avatar

      I think this is possible.

      For rotation, you have to rotate them after adding meta data to the tree, or after you connect two trees side by side.

      To selectively colorize different clades, you can use this tutorial: https://yulab-smu.top/treedata-book/chapter6.html

  7. Dylan H. Cohen Avatar
    Dylan H. Cohen

    Is there anyway to connect the line from the end of the tip label to the other tree so the line does not cross thru the tip label? Thanks1

    1. Sorry for the late response. I think that is possible. I’ll try and come back to this. Thanks for the idea!

    2. Yes, if you check how it is plotting the line, geom_line(aes(x, y, group=label), data=green_tree, color='#009E73'), you just need to update the x coordinate in the green_tree data set.

      For each label, there are two entries here, one for tree 1 and another for tree 2.

      If you add some constant value to x-coordinate for tree 1 and subtract the same value from y-coordinate, that will do!

      Sorry for the late reply 🙁

  8. What if you have two different trees that do not have the same tip labels. For example a phylogenetic tree based on a core genome and then a protein based tree and then you want to show which genomes in the core tree that harbours the protein in the second tree. Thanks!

    1. That is definitely possible. Please give me some time, and I’ll update the main post. Sorry for the late reply. Best.

  9. Fernando Hayashi Avatar
    Fernando Hayashi

    I was not able to find how the variable dd1 was created. Could you please clarify?

    1. Arafat Avatar

      Sorry about that! I just added it to the main post:

      dd <- bind_rows(d1, d2) %>%
      filter(isTip == TRUE)
      dd1 <- as.data.frame(dd)

      1. Fernando Hayashi Avatar
        Fernando Hayashi

        Thank you very much!

  10. Hi, really clear description of how to draw tanglegrams within R. Would it be possible to extend this to more than two trees?

    1. Arafat Avatar

      That is really interesting idea. I think that is technically possible. Let me try that, I’ll come back to you soon! Thanks for reading 🙂

  11. Hi, this is great thanks. How would you edit your code to link the tips from the two trees not by your meta file, but by matching tiplabels? Thanks! Lily

    1. Arafat Avatar

      Yes you can do that. You do not need to do any subset, and use the following command in R instead:

      pp + geom_line(aes(x, y, group=label), data=dd1)

      Please check the article, I have updated it!

      Thanks for reading 🙂

Leave a Reply

Learn Python for Bioinformatics

I have created a set of worksheets that give a quick overview of Python for Bioinformatics (as well as intro to UNIX). You just have to give it 3-6 hours, and you will know the essentials!

Join 94 other subscribers