Tuesday, December 31, 2024

Learning about the structure of the "phylo" object class through a phylogenetic Hanukkah menorah

Several years ago I posted a phylogenetic Christmas tree, adapted from one cleverly created by Gustavo Ballen.

This year, Christmas has passed – but Hanukkah lasts until the 2nd of January, 2025. One of the symbols of Hanukkah is the the menorah. As a gentile, I profess to be ignorant of its cultural & religious significance – however, it does look suspiciously like a phylogeny!

Here, for example, is a menorah for sale on Amazon:

To recreate this as a "phylo" object, I’m first going to re-draw it as follows.

Then, I’ll number the tips 1 through 9:

Finally, I’ll number the nodes 10 through 14, as follows:

Having drawn it out, I can easily re-write my menorah “tree” as an edge matrix, as follows.

edge<-matrix(c(
  10,11,
  11,1,
  11,12,
  11,9,
  12,2,
  12,13,
  12,8,
  13,3,
  13,14,
  13,7,
  14,4,
  14,5,
  14,6),13,2,byrow=TRUE)

To create a complete "phylo" object, we just need to combine this with a vector of tip labels, and a numeric value giving the number of nodes, as follows:

phy<-list(
  edge=edge,
  tip.label=1:9,
  Nnode=5
)

Then assign a class attribute:

class(phy)<-c("menorah","phylo")

(There’s no "menorah" class methods. I just did that for fun.)

Let’s load phytools and see if we’re on the right track.

library(phytools)
phy
## 
## Phylogenetic tree with 9 tips and 5 internal nodes.
## 
## Tip labels:
##   1, 2, 3, 4, 5, 6, ...
## 
## Rooted; no branch length.
par(mar=rep(2.1,4))
plot(phy,direction="upwards",show.tip.label=FALSE)
nodelabels()
tiplabels()

plot of chunk unnamed-chunk-7

(We should be too alarmed that our “stem” branch is not plotted. Since we didn’t assign it a length, ape::plot.phylo uses compute.brlen internally, which assigns a length of zero to unbranching stem edges.)

Let’s assign our own edge lengths to (more or less) match the menorah from Amazon. These will take the form of a vector in the order of our rows of phy$edge, as follows.

phy$edge
##       [,1] [,2]
##  [1,]   10   11
##  [2,]   11    1
##  [3,]   11   12
##  [4,]   11    9
##  [5,]   12    2
##  [6,]   12   13
##  [7,]   12    8
##  [8,]   13    3
##  [9,]   13   14
## [10,]   13    7
## [11,]   14    4
## [12,]   14    5
## [13,]   14    6
phy$edge.length<-c(0.2,0.5,0.1,0.5,0.4,0.1,0.4,0.3,0.1,0.3,
  0.2,0.3,0.2)
plotTree(phy,direction="upwards",lwd=4)

plot of chunk unnamed-chunk-9

Great. We’re totally on the right track. I think we can do better. How about this:

sigmoidPhylogram(phy,direction="upwards",lwd=20)
pp<-get("last_plot.phylo",envir=.PlotPhyloEnv)
sigmoidPhylogram(phy,direction="upwards",lwd=16,
  color="#C0C0C0",add=TRUE,ftype="off",ylim=pp$y.lim)

plot of chunk unnamed-chunk-10

Finally, let’s get fancy. (Edit: updated to reflect the correct lighting sequence for the 8th night of Hanukkah, as I understand it.)

sigmoidPhylogram(phy,direction="upwards",lwd=14,
  ylim=c(0,0.92),ftype="off",color="blue",m1=0.06,b=5)
pp<-get("last_plot.phylo",envir=.PlotPhyloEnv)
## star of David
polygon(x=pp$xx[5]+c(0,0.5,-0.5),y=pp$yy[5]-c(0,0.17,0.17),
  border="transparent",col="blue")
polygon(x=pp$xx[5]+c(0,0.5,-0.5),y=pp$yy[5]-c(0.22,0.07,0.07),
  border="transparent",col="blue")
polygon(x=pp$xx[5]+c(0,0.5,-0.5),y=pp$yy[5]-c(0,0.17,0.17),
  border="grey")
polygon(x=pp$xx[5]+c(0,0.5,-0.5),y=pp$yy[5]-c(0.22,0.07,0.07),
  border="grey")
## candle bases
for(i in 1:Ntip(phy)){
  polygon(x=pp$xx[i]+c(-0.15,0.15,0.15,-0.15),
    y=pp$yy[i]+c(0,0,0.05,0.05),col="#C0C0C0")
}
## flames
x1<-c(0,1,2,3)/30
x2<-c(0,0.5,1.5,2.5,3)/30
y1<-(c(2,2.5,2.1,2.3)-1.6)/4
y2<-(c(1.2,0.9,0.8,1.5,2.3)-1.6)/4
f1<-smooth.spline(x1,y1,df=4)
left<-predict(f1,x=seq(0,max(x1),length.out=20))
f2<-smooth.spline(x2,y2,df=4)
right<-predict(f2,x=seq(0,max(x1),length.out=20))
xx<-c(left$y,right$y[20:1])
yy<-c(left$x,right$x[20:1])
lighting_sequence<-c(5,1,2,3,4,6,7,8,9)
for(i in lighting_sequence){
  polygon(x=pp$xx[i]+xx,y=pp$yy[i]+0.05+yy,
    col="gold")
}
## base
polygon(x=c(3.75,6.25,5.5,5.25,4.75,4.5),
  y=c(-0.03,-0.03,0.025,0.1,0.1,0.025),
  border="transparent",col="blue")

plot of chunk unnamed-chunk-11

(Just don’t mess with the aspect ratio, or all bets are off!)

As a side lesson on how S3 classes work, let’s combine all of this into a class method for the object class "menorah".

plot.menorah<-function(phy,...){
  phy<-if(is.null(attr(phy,"order"))) reorder(phy) else phy
  ## rescale our tree & add unbranching root (if necessary)
  if(sum(phy$edge[,1]==(Ntip(phy)+1))>1){
    phy$edge.length<-phy$edge.length/max(nodeHeights(phy))*0.6
    phy$root.edge<-0.2
    phy<-rootedge.to.singleton(phy)
  } else {
    phy$edge.length<-phy$edge.length/max(nodeHeights(phy))*0.8
  }
  ## plot in sigmoid style
  sigmoidPhylogram(phy,direction="upwards",lwd=14,
    ylim=c(0,0.92),ftype="off",color="blue",m1=0.06,b=5)
  pp<-get("last_plot.phylo",envir=.PlotPhyloEnv)
  ## add candle bases
  for(i in 1:Ntip(phy)){
    polygon(x=pp$xx[i]+c(-0.15,0.15,0.15,-0.15),
      y=pp$yy[i]+c(0,0,0.05,0.05),col="#C0C0C0")
  }
  ## add flames
  x1<-c(0,1,2,3)/30
  x2<-c(0,0.5,1.5,2.5,3)/30
  y1<-(c(2,2.5,2.1,2.3)-1.6)/4
  y2<-(c(1.2,0.9,0.8,1.5,2.3)-1.6)/4
  f1<-smooth.spline(x1,y1,df=4)
  left<-predict(f1,x=seq(0,max(x1),length.out=20))
  f2<-smooth.spline(x2,y2,df=4)
  right<-predict(f2,x=seq(0,max(x1),length.out=20))
  xx<-c(left$y,right$y[20:1])
  yy<-c(left$x,right$x[20:1])
  for(i in 1:Ntip(phy)){
    polygon(x=pp$xx[i]+xx,y=pp$yy[i]+0.05+yy,
      col="gold")
  }
  ## base
  polygon(x=c(-1.25,1.25,0.5,0.25,-0.25,-0.5)+pp$xx[Ntip(phy)+1],
    y=c(-0.03,-0.03,0.025,0.1,0.1,0.025),
    border="transparent",col="blue")
}

Now let’s load a random tree, update the class, and test it. (Once again, I haven’t done anything to ensure this is insensitive to our aspect ratio, so results may vary!)

data(sunfish.tree)
sunfish.tree<-as.phylo(sunfish.tree)
class(sunfish.tree)<-c("menorah",class(sunfish.tree))
plot(sunfish.tree)

plot of chunk unnamed-chunk-13

Happy Hanukkah!

No comments:

Post a Comment

Note: due to the very large amount of spam, all comments are now automatically submitted for moderation.