Search code examples
r3ddendrogramrgl

How to create polar dendrogram in 3D in rgl window in R


I want to create a polar dendrogram in 3D in rgl window in R.

I adpated the code here, which was orginally intended for creating a 2D dendrogram (not polar), to create the dendrogram in 3D rgl window:

a <- list()  # initialize empty object
# define merging pattern: 
#    negative numbers are leaves, 
#    positive are merged clusters (defined by row number in $merge)
a$merge <- matrix(c(-1, -2,
                    -3, -4,
                     1,  2), nc=2, byrow=TRUE ) 
a$height <- c(1, 1.5, 3)    # define merge heights
a$order <- 1:4              # order of leaves(trivial if hand-entered)
a$labels <- LETTERS[1:4]    # labels of leaves
class(a) <- "hclust"        # make it an hclust object
plot(a)                     # look at the result   

# Convert to a dendrogram object.
ad <- as.dendrogram(a)

# dend_data contains segment information
library(ggdendro)
dend_data <- dendro_data(ad, type = "rectangle")

nodes <- dend_data$segments
# Append z value of 0 so that the dendrogram lies in a 2D plane embedded in 3D space.
nodes_3d <- cbind(nodes, z = 0, zend = 0)
nodes_3d <- nodes_3d[,c(1, 2, 5, 3, 4, 6)]

# Convert nodes_3d to nodes_3dLong, which is used by segments3d function to draw lines.
colnames(nodes_3d) <- NULL
nodes_3da <- nodes_3d[,1:3]
nodes_3db <- nodes_3d[,4:6]
nodes_3dLong <- do.call(rbind, lapply(1:nrow(nodes_3d), 
    function(i) rbind(unlist(c(nodes_3da[i,])), 
                      unlist(c(nodes_3db[i,])))))
# Plot the dendrogram in 3D.
library(rgl)
open3d()
segments3d(nodes_3dLong)

The above code (fully reproducible) produces dendrogram in 3D space in rgl window. I want to convert this dendrogram to polar dendrogram in rgl window. The polar dendrogram should still lie in a 2D plane in 3D space. The only difference is that it is a polar dendrogram. For 2D images, coord_polar in ggplot2 is used to create polar dendrogram. But I do not know how to do this in 3D.

P.S. After converting to 3D polar dendrograms, I want to add 3D meshes at specified position through translate3d. So I wish any solution open to the possibility of further editing this 3D polar dendrogram by adding new 3D meshes. Thank you.


Solution

  • Here's some code that converts the nodes variable from your calculation by adding polar coordinates to it, and then draws the tree using that:

    a <- list()  # initialize empty object
    # define merging pattern: 
    #    negative numbers are leaves, 
    #    positive are merged clusters (defined by row number in $merge)
    a$merge <- matrix(c(-1, -2,
                        -3, -4,
                        1,  2), nc=2, byrow=TRUE ) 
    a$height <- c(1, 1.5, 3)    # define merge heights
    a$order <- 1:4              # order of leaves(trivial if hand-entered)
    a$labels <- LETTERS[1:4]    # labels of leaves
    class(a) <- "hclust"        # make it an hclust object
    plot(a)                     # show it
    
    # Convert to a dendrogram object.
    ad <- as.dendrogram(a)
    
    # dend_data contains segment information
    library(ggdendro)
    

    dend_data <- dendro_data(ad, type = "rectangle")
    
    nodes <- dend_data$segments
    
    # Set the gap between the ends of the tree
    gap <- 0
    # Set the offset from the center.  
    offset <- 0
    
    radius <- with(nodes, max(c(y, yend)) + offset)
    circ <- with(nodes, max(c(x, xend)) + gap)
    
    # Convert to polar coordinates
    nodes$theta <- with(nodes, 2*pi*x/circ)
    nodes$thetaend <- with(nodes, 2*pi*xend/circ)
    nodes$r     <- with(nodes, (radius - y)/radius)
    nodes$rend  <- with(nodes, (radius - yend)/radius)
    
    # Extract the horizontal and vertical segments
    horiz <- subset(nodes, y == yend)
    vert <- subset(nodes, x == xend)
    
    library(rgl)
    
    open3d()
    #> glX 
    #>   1
    
    # Draw the vertical segments, which are still segments
    x     <- with(vert, as.numeric(rbind(r*cos(theta), rend*cos(theta))))
    y     <- with(vert, as.numeric(rbind(r*sin(theta), rend*sin(theta))))
    segments3d(x, y, z = 0)
    
    # Draw the horizontal segments, which are now arcs.  Zero
    # radius arcs are dropped
    horiz <- subset(horiz, r > 0)
    with(horiz, arc3d(from = cbind(r*cos(theta), r*sin(theta), 0),
                      to = cbind(r*cos(thetaend), r*sin(thetaend), 0),
                      center = c(0, 0, 0)))
    
    # Draw the labels
    labels <- dend_data$labels
    labels$theta <- with(labels, 2*pi*x/circ)
    # Add a bit to the y so the label doesn't overlap the segment
    labels$r     <- with(labels, (radius - y)/radius + 0.1)
    with(labels, text3d(r*cos(theta), r*sin(theta), 0, label))
    
    # Draw a circle around the whole thing
    margin <- 0.25  # The gap below the leaves
    theta <- seq(from = 0, to = 2*pi, length = 50)
    r <- 1 + margin
    lines3d(r*cos(theta), r*sin(theta), 0)
    

    Created on 2023-04-12 with reprex v2.0.2

    The positioning of the labels should give you a hint as to how to position your meshes.

    BTW, there's one possible bug in this code: if any of the arcs go more than half-way around the circle, they'll be drawn in the wrong direction. If this is a problem, break up those pieces into several arcs.