Tuesday, February 21, 2017

More on labeling nodes using ape::nodelabels & phytools

The colleague question that inspired my recent post on interactively adding node labels to a tree was actually must simpler & is as follows:

“A student of mine has a 250 taxon tree that we want to number the nodes for on the figure, starting with "1” (preferably the root, but we can work with most any logical system) through 249. Is there an easy way in phytools?“

This is indeed pretty straightforward. I'm going to work with a small tree of 50 taxa - just to not overwhelm the size of plot window I can create on my blog - but the principle is identical.

First of all - we can easily do it with ape::nodelabels.

library(ape)
tree
## 
## Phylogenetic tree with 50 tips and 49 internal nodes.
## 
## Tip labels:
##  t8, t9, t7, t3, t17, t27, ...
## 
## Rooted; includes branch lengths.
plot(tree,no.margin=TRUE,edge.width=2,cex=0.7)
nodelabels(text=1:tree$Nnode,node=1:tree$Nnode+Ntip(tree))

plot of chunk unnamed-chunk-1

There are many different options with nodelabels. For instance, we can plot our tree in "fan" style, and our node labels within circles:

plot(tree,no.margin=TRUE,edge.width=2,type="fan",cex=0.9)
nodelabels(text=1:tree$Nnode,node=1:tree$Nnode+Ntip(tree),frame="circle")

plot of chunk unnamed-chunk-2

We don't need to use frames around our labels. We can instead offset them from the plotted edges:

plot(tree,no.margin=TRUE,edge.width=2,cex=0.7)
nodelabels(text=1:tree$Nnode,node=1:tree$Nnode+Ntip(tree),
    frame="none",adj=c(1.1,-0.4))

plot of chunk unnamed-chunk-3

Equally well, we could employ the new function that I have just added to the phytools package. For instance:

library(phytools)
packageVersion("phytools")
## [1] '0.5.77'
plotTree(tree,type="fan",fsize=0.9,ftype="i")
labelnodes(text=1:tree$Nnode,node=1:tree$Nnode+Ntip(tree),
    interactive=FALSE,circle.exp=0.9,cex=0.8)

plot of chunk unnamed-chunk-4

I like that.

Interactive node labeling using phytools

When it comes to labeling internal nodes on a phylogenetic tree in R, the function nodelabels in the ape package can pretty much do it all. Nonetheless, when I was contacted recently by a colleague I realized there was space for some additional functionality - specifically, in terms of allowing the user to interact with the plotting device to determine the location of said labels - something that is not currently possible using ape::nodelabels (so far as I can tell).

The code below consists of two different functions. The first returns the index of the closest node on the plotting device for a currently plotted phylogeny. The second writes a label to that node, with a few different options from nodelabels. Note that getnode could easily be combined with ape::nodelabels to take advantage of all the functionality of nodelabels but in an interactive context.

getnode<-function(...){
    if(hasArg(env)) env<-list(...)$env
    else env<-get("last_plot.phylo",envir=.PlotPhyloEnv)
    xy<-unlist(locator(n=1))
    points(xy[1],xy[2])
    d<-sqrt((xy[1]-env$xx)^2+(xy[2]-env$yy)^2)
    ii<-which(d==min(d))[1]
    ii
}

labelnodes<-function(text,node=NULL,interactive=TRUE,
    shape=c("circle","ellipse","rect"),...){
    shape<-shape[1]
    if(hasArg(circle.exp)) circle.exp<-list(...)$circle.exp
    else circle.exp<-1.3
    if(hasArg(rect.exp)) rect.exp<-list(...)$rect.exp
    else rect.exp<-1.6
    if(hasArg(cex)) cex<-list(...)$cex
    else cex<-1
    obj<-get("last_plot.phylo",envir=.PlotPhyloEnv)
    h<-cex*strheight("A")
    w<-cex*strwidth(text)
    rad<-circle.exp*h*diff(par()$usr[1:2])/diff(par()$usr[3:4])
    if(is.null(node)){
        if(!interactive){
            cat("No nodes provided. Setting interactive mode to TRUE.\n")
            interactive<-TRUE
        }
        node<-vector(length=length(text))
    }
    for(i in 1:length(text)){
        if(interactive){
            cat(paste("Click on the node you would like to label ",
                text[i],".\n",sep=""))
            flush.console()
            ii<-getnode(env=obj)
            node[i]<-ii
        } else ii<-node[i]
        if(shape=="circle")
            draw.circle(obj$xx[ii],obj$yy[ii],rad,col="white")
        else if(shape=="ellipse")
            draw.ellipse(obj$xx[ii],obj$yy[ii],0.8*w[i],h,
                col="white")
        else if(shape=="rect")
            rect(xleft=obj$xx[ii]-0.5*rect.exp*w[i],
                ybottom=obj$yy[ii]-0.5*rect.exp*h,
                xright=obj$xx[ii]+0.5*rect.exp*w[i],
                ytop=obj$yy[ii]+0.5*rect.exp*h,col="white",
                ljoin=1)
        text(obj$xx[ii],obj$yy[ii],label=text[i],cex=cex)
    }
    invisible(node)
}
library(phytools)
library(plotrix)
text
vertebrates<-read.tree(text=text)
plotTree(vertebrates)
labels<-c("Cartilaginous fish",
    "Ray-finned fish",
    "Lobe-finned fish",
    "Anurans",
    "Reptiles (& birds)",
    "Birds",
    "Mammals",
    "Eutherians")
labels
nodes<-labelnodes(text=labels,shape="ellipse",cex=0.8)

(Click for full screen version.)

plotTree(vertebrates,fsize=0.8)
labelnodes(node=nodes,text=labels,shape="ellipse",cex=0.7,interactive=FALSE)

plot of chunk unnamed-chunk-3

That's it.