a76f35c1 |
##' @importFrom ape reorder.phylo
|
216e89e6 |
layout.unrooted <- function(model, branch.length="branch.length", layout.method="equal_angle", MAX_COUNT=5, ...) {
|
96532506 |
|
f5b8c5ea |
df <- switch(layout.method,
|
642e2cf8 |
equal_angle = layoutEqualAngle(model, branch.length),
|
b8275604 |
daylight = layoutDaylight(model, branch.length, MAX_COUNT),
ape = layoutApe(model, branch.length)
|
f5b8c5ea |
)
|
b91f9a97 |
|
40f0f078 |
return(df)
}
|
a76f35c1 |
|
b681f0b4 |
set_branch_length_cladogram <- function(tree) {
phylo <- as.phylo(tree)
edge <- phylo$edge
xpos <- getXcoord_no_length(phylo)
phylo$edge.length <- xpos[edge[,2]] - xpos[edge[,1]]
if (is(tree, "phylo")) {
return(phylo)
} else if (is(tree, "treedata")) {
tree@phylo <- phylo
return(tree)
}
message("unknown tree object, fail to set branch length for cladogram...")
return(tree)
}
|
a76f35c1 |
|
40f0f078 |
##' 'Equal-angle layout algorithm for unrooted trees'
##'
|
a6e5cc92 |
##' @references
|
40f0f078 |
##' "Inferring Phylogenies" by Joseph Felsenstein.
|
a6e5cc92 |
##'
|
40f0f078 |
##' @title layoutEqualAngle
|
b91f9a97 |
##' @param model tree object, e.g. phylo or treedata
|
40f0f078 |
##' @param branch.length set to 'none' for edge length of 1. Otherwise the phylogenetic tree edge length is used.
##' @return tree as data.frame with equal angle layout.
|
53ab3069 |
layoutEqualAngle <- function(model, branch.length = "branch.length"){
|
642e2cf8 |
tree <- as.phylo(model)
|
b681f0b4 |
|
b91f9a97 |
if (! is.null(tree$edge.length)) {
if (anyNA(tree$edge.length)) {
warning("'edge.length' contains NA values...\n## setting 'edge.length' to NULL automatically when plotting the tree...")
tree$edge.length <- NULL
}
}
|
80b1a5bb |
|
b7f2b5a1 |
if (is.null(tree$edge.length) || branch.length == "none") {
|
b91f9a97 |
tree <- set_branch_length_cladogram(tree)
}
|
53ab3069 |
N <- treeio::Nnode2(tree)
brlen <- numeric(N)
|
b7f2b5a1 |
brlen[tree$edge[,2]] <- tree$edge.length
|
53ab3069 |
root <- tidytree::rootnode(tree)
|
b91f9a97 |
## Convert Phylo tree to data.frame.
## df <- as.data.frame.phylo_(tree)
|
21cbdbae |
df <- as_tibble(model) %>%
|
f3952913 |
mutate(isTip = ! .data$node %in% .data$parent)
|
e9896b76 |
## NOTE: Angles (start, end, angle) are in half-rotation units (radians/pi or degrees/180)
## create and assign NA to the following fields.
|
53ab3069 |
df$x <- 0
df$y <- 0
df$start <- 0 # Start angle of segment of subtree.
df$end <- 0 # End angle of segment of subtree
df$angle <- 0 # Orthogonal angle to beta for tip labels.
|
e9896b76 |
## Initialize root node position and angles.
df[root, "x"] <- 0
df[root, "y"] <- 0
df[root, "start"] <- 0 # 0-degrees
df[root, "end"] <- 2 # 360-degrees
df[root, "angle"] <- 0 # Angle label.
|
b7f2b5a1 |
df$branch.length <- brlen[df$node] # for cladogram
|
e9896b76 |
## Get number of tips for each node in tree.
|
1dd645ce |
## nb.sp <- sapply(1:N, function(i) length(get.offspring.tip(tree, i)))
|
a0450f48 |
## self_include = TRUE to return itself if the input node is a tip
|
53ab3069 |
nb.sp <- vapply(1:N, function(i) length(offspring(tree, i, tiponly = TRUE, self_include = TRUE)), numeric(1))
|
e9896b76 |
## Get list of node id's.
nodes <- getNodes_by_postorder(tree)
for(curNode in nodes) {
## Get number of tips for current node.
curNtip <- nb.sp[curNode]
## Get array of child node indexes of current node.
|
a0450f48 |
## children <- getChild(tree, curNode)
children <- treeio::child(tree, curNode)
|
e9896b76 |
## Get "start" and "end" angles of a segment for current node in the data.frame.
start <- df[curNode, "start"]
end <- df[curNode, "end"]
|
fc03966c |
cur_x = df[curNode, "x"]
cur_y = df[curNode, "y"]
for (child in children) {
|
e9896b76 |
## Get the number of tips for child node.
ntip.child <- nb.sp[child]
## Calculated in half radians.
## alpha: angle of segment for i-th child with ntips_ij tips.
## alpha = (left_angle - right_angle) * (ntips_ij)/(ntips_current)
alpha <- (end - start) * ntip.child / curNtip
## beta = angle of line from parent node to i-th child.
beta <- start + alpha / 2
|
b681f0b4 |
length.child <- df[child, "branch.length"]
|
e9896b76 |
## update geometry of data.frame.
## Calculate (x,y) position of the i-th child node from current node.
|
fc03966c |
df[child, "x"] <- cur_x + cospi(beta) * length.child
df[child, "y"] <- cur_y + sinpi(beta) * length.child
|
e3ea6fc3 |
## Calculate orthogonal angle to beta for tip label.
|
e9896b76 |
df[child, "angle"] <- -90 - 180 * beta * sign(beta - 1)
## Update the start and end angles of the childs segment.
df[child, "start"] <- start
df[child, "end"] <- start + alpha
start <- start + alpha
}
|
a6e5cc92 |
}
|
a0450f48 |
tree_df <- as_tibble(df)
class(tree_df) <- c("tbl_tree", class(tree_df))
return(tree_df)
|
40f0f078 |
}
|
a76f35c1 |
|
40f0f078 |
##' Equal daylight layout method for unrooted trees.
|
a6e5cc92 |
##'
|
40f0f078 |
##' #' @title
|
b91f9a97 |
##' @param model tree object, e.g. phylo or treedata
|
40f0f078 |
##' @param branch.length set to 'none' for edge length of 1. Otherwise the phylogenetic tree edge length is used.
|
35e8c189 |
##' @param MAX_COUNT the maximum number of iterations to run (default 5)
|
40f0f078 |
##' @return tree as data.frame with equal angle layout.
|
a6e5cc92 |
##' @references
|
40f0f078 |
##' The following aglorithm aims to implement the vague description of the "Equal-daylight Algorithm"
##' in "Inferring Phylogenies" pp 582-584 by Joseph Felsenstein.
|
a6e5cc92 |
##'
|
40f0f078 |
##' ```
##' Leafs are subtrees with no children
##' Initialise tree using equal angle algorithm
##' tree_df = equal_angle(tree)
##'
##' nodes = get list of nodes in tree_df breadth-first
##' nodes = remove tip nodes.
|
a6e5cc92 |
##'
|
40f0f078 |
##' ```
|
216e89e6 |
layoutDaylight <- function(model, branch.length, MAX_COUNT=5 ){
|
642e2cf8 |
tree <- as.phylo(model)
|
80b1a5bb |
|
f5b8c5ea |
## How to set optimal
|
9a1c4d17 |
MINIMUM_AVERAGE_ANGLE_CHANGE <- 0.05
|
a6e5cc92 |
|
40f0f078 |
|
f5b8c5ea |
## Initialize tree.
|
642e2cf8 |
tree_df <- layoutEqualAngle(model, branch.length)
|
40f0f078 |
|
f5b8c5ea |
## nodes = get list of nodes in tree_df
## Get list of node id's.
## nodes <- getNodes_by_postorder(tree)
|
9a1c4d17 |
## nodes <- getSubtree.df(tree_df, root)
|
a6e5cc92 |
|
f5b8c5ea |
## Get list of internal nodes
## nodes <- tree_df[tree_df$IsTip != TRUE]$nodes
|
a6e5cc92 |
|
f5b8c5ea |
nodes <- getNodesBreadthFirst.df(tree_df)
## select only internal nodes
internal_nodes <- tree_df[!tree_df$isTip,]$node
## Remove tips from nodes list, but keeping order.
nodes <- intersect(nodes, internal_nodes)
|
a6e5cc92 |
|
f5b8c5ea |
ave_change <- 1.0
|
2fa23352 |
for (i in seq_len(MAX_COUNT)) {
|
f5b8c5ea |
## Reset max_change after iterating over tree.
total_max <- 0.0
|
2fa23352 |
for(currentNode_id in nodes){
|
f5b8c5ea |
result <- applyLayoutDaylight(tree_df, currentNode_id)
tree_df <- result$tree
total_max <- total_max + result$max_change
}
|
9a1c4d17 |
# Calculate the running average of angle changes.
|
2fa23352 |
ave_change <- total_max / length(nodes)
|
b681f0b4 |
message('Average angle change [',i,'] ', ave_change)
|
2fa23352 |
if (ave_change <= MINIMUM_AVERAGE_ANGLE_CHANGE) break
|
f5b8c5ea |
}
|
a6e5cc92 |
|
a0450f48 |
tree_df <- as_tibble(tree_df)
class(tree_df) <- c("tbl_tree", class(tree_df))
return(tree_df)
|
40f0f078 |
}
|
a76f35c1 |
|
40f0f078 |
##' Apply the daylight alorithm to adjust the spacing between the subtrees and tips of the
##' specified node.
|
a6e5cc92 |
##'
|
40f0f078 |
##' @title applyLayoutDaylight
##' @param df tree data.frame
##' @param node_id is id of the node from which daylight is measured to the other subtrees.
|
a6e5cc92 |
##' @return list with tree data.frame with updated layout using daylight algorithm and max_change angle.
|
2fa23352 |
##' @importFrom rlang .data
|
a6e5cc92 |
##
##
## ```
## for node in nodes {
## if node is a leaf {
## next
## }
##
## subtrees = get subtrees of node
##
## for i-th subtree in subtrees {
## [end, start] = get left and right angles of tree from node id.
## angle_list[i, 'left'] = end
## angle_list[i, 'beta'] = start - end # subtree arc angle
## angle_list[i, 'index'] = i-th # index of subtree/leaf
## }
##
## sort angle_list by 'left' column in ascending order.
##
## D = 360 - sum( angle_list['beta'] ) # total daylight angle
## d = D / |subtrees| # equal daylight angle.
##
## new_L = left angle of first subtree.
##
## for n-th row in angle_list{
## # Calculate angle to rotate subtree/leaf to create correct daylight angle.
## new_left_angle = new_left_angle + d + angle_list[n, 'beta']
## Calculate the difference between the old and new left angles.
## adjust_angle = new_left_angle - angle_list[n, 'left']
##
## index = angle_list['index']
## rotate subtree[index] wrt n-th node by adjust_angle
## }
## }
## }
## ```
|
9a1c4d17 |
applyLayoutDaylight <- function(df, node_id){
|
40f0f078 |
# Get lists of node ids for each subtree, including rest of unrooted tree.
subtrees <- getSubtreeUnrooted.df(df, node_id)
# Return tree if only 2 or less subtrees to adjust.
if(length(subtrees) <= 2){
|
2fa23352 |
return( list(tree = df, max_change = 0.0) )
|
40f0f078 |
}
|
a6e5cc92 |
|
40f0f078 |
# Find start and end angles for each subtree.
# subtrees = get subtrees of node
# for i-th subtree in subtrees {
|
2fa23352 |
angle_list = purrr::map_dfr(subtrees, ~{
getTreeArcAngles(df, node_id, .x) %>% dplyr::bind_rows()
}) %>% dplyr::transmute(
|
fc03966c |
left = .data$left,
|
2fa23352 |
beta = .data$left - .data$right,
beta = ifelse(.data$beta < 0, .data$beta + 2, .data$beta),
subtree_id = seq_len(nrow(.))
) %>% dplyr::arrange(.data$left)
|
40f0f078 |
# sort angle_list by 'left angle' column in ascending order.
# D = 360 - sum( angle_list['beta'] ) # total day
# d = D / |subtrees| # equal daylight angle.
|
2fa23352 |
total_daylight <- 2 - sum(angle_list[['beta']])
|
40f0f078 |
d <- total_daylight / length(subtrees)
|
a6e5cc92 |
|
40f0f078 |
# Initialise new left-angle as first subtree left-angle.
|
2fa23352 |
new_left_angle <- angle_list$left[1]
|
40f0f078 |
# Adjust angles of subtrees and tips connected to current node.
# for n-th row in angle_list{
# Skip the first subtree as it is not adjusted.
|
2fa23352 |
max_change <- 0.0
|
40f0f078 |
for (i in 2:nrow(angle_list) ) {
# Calculate angle to rotate subtree/leaf to create correct daylight angle.
|
2fa23352 |
new_left_angle <- new_left_angle + d + angle_list$beta[i]
|
40f0f078 |
# Calculate the difference between the old and new left angles.
|
2fa23352 |
adjust_angle <- new_left_angle - angle_list$left[i]
|
a6e5cc92 |
|
40f0f078 |
max_change <- max(max_change, abs(adjust_angle))
#cat('Adjust angle:', abs(adjust_angle), ' Max change:', max_change ,'\n')
|
a6e5cc92 |
|
40f0f078 |
# rotate subtree[index] wrt current node
|
2fa23352 |
subtree_id <- angle_list$subtree_id[i]
|
40f0f078 |
subtree_nodes <- subtrees[[subtree_id]]$subtree
# update tree_df for all subtrees with rotated points.
df <- rotateTreePoints.df(df, node_id, subtree_nodes, adjust_angle)
}
|
a6e5cc92 |
return( list(tree = df, max_change = max_change) )
|
40f0f078 |
}
|
a6e5cc92 |
##' Find the right (clockwise rotation, angle from +ve x-axis to furthest subtree nodes) and
|
1540db03 |
##' left (anti-clockwise angle from +ve x-axis to subtree) Returning arc angle in `[0, 2]` (0 to 360) domain.
|
a6e5cc92 |
##'
|
40f0f078 |
##' @title getTreeArcAngles
##' @param df tree data.frame
##' @param origin_id node id from which to calculate left and right hand angles of subtree.
|
9a1c4d17 |
##' @param subtree named list of root id of subtree (node) and list of node ids for given subtree (subtree).
|
1540db03 |
##' @return named list with right and left angles in range `[0,2]` i.e 1 = 180 degrees, 1.5 = 270 degrees.
|
40f0f078 |
getTreeArcAngles <- function(df, origin_id, subtree) {
|
fc03966c |
df_x = df$x
df_y = df$y
x_origin = df_x[origin_id]
y_origin = df_y[origin_id]
|
b681f0b4 |
## Initialise variables
theta_child <- 0.0
subtree_root_id <- subtree$node
subtree_node_ids <- subtree$subtree
## Initialise angle from origin node to parent node.
## If subtree_root_id is child of origin_id
|
a0450f48 |
## if (subtree_root_id %in% getChild.df(df, origin_id)) {
|
aff471e1 |
if (subtree_root_id %in% tidytree:::child.tbl_tree(df, origin_id)$node) {
|
b681f0b4 |
## get angle from original node to parent of subtree.
|
fc03966c |
theta_left <- getNodeAngle.vector(x_origin, y_origin, df_x[subtree_root_id], df_y[subtree_root_id])
|
b681f0b4 |
theta_right <- theta_left
} else if( subtree_root_id == origin_id ){
## Special case.
## get angle from parent of subtree to children
|
a0450f48 |
## children_ids <- getChild.df(df, subtree_root_id)
|
aff471e1 |
children_ids <- tidytree:::child.tbl_tree(df, subtree_root_id)$node
|
b681f0b4 |
if(length(children_ids) == 2){
## get angles from parent to it's two children.
|
fc03966c |
theta1 <- getNodeAngle.vector(x_origin, y_origin, df_x[children_ids[1]], df_y[children_ids[1]])
theta2 <- getNodeAngle.vector(x_origin, y_origin, df_x[children_ids[2]], df_y[children_ids[2]])
|
b681f0b4 |
delta <- theta1 - theta2
## correct delta for points crossing 180/-180 quadrant.
if(delta > 1){
delta_adj = delta - 2
} else if(delta < -1){
delta_adj = delta + 2
} else{
delta_adj <- delta
}
if(delta_adj >= 0){
theta_left = theta1
theta_right = theta2
} else if(delta_adj < 0){
theta_left = theta2
theta_right = theta1
}
}else{
## subtree only has one child node.
|
fc03966c |
theta_left <- getNodeAngle.vector(x_origin, y_origin, df_x[children_ids[1]], df_y[children_ids[1]])
|
b681f0b4 |
theta_right <- theta_left
}
} else {
## get the real root of df tree to initialise left and right angles.
tree_root <- getRoot.df(df)
if( !is.na(tree_root) & is.numeric(tree_root) ){
|
fc03966c |
theta_left <- getNodeAngle.vector(x_origin, y_origin, df_x[tree_root], df_y[tree_root])
|
b681f0b4 |
theta_right <- theta_left
} else{
|
9a1c4d17 |
print('ERROR: no root found!')
theta_left <- NA
}
|
40f0f078 |
}
|
9a1c4d17 |
# no parent angle found.
|
fc03966c |
# Subtree has to have 1 or more nodes to compare.
if (is.na(theta_left) || (length(subtree_node_ids) == 0)){
|
1418c053 |
return(c('left' = 0, 'right' = 0))
|
9a1c4d17 |
}
|
40f0f078 |
# create vector with named columns
# left-hand and right-hand angles between origin node and the extremities of the tree nodes.
|
1418c053 |
arc <- c('left' = theta_left, 'right' = theta_right)
|
a6e5cc92 |
|
40f0f078 |
# Calculate the angle from the origin node to each child node.
# Moving from parent to children in depth-first traversal.
|
fc03966c |
# Skip if parent_id is a tip or parent and child node are the same.
subtree_node_ids = subtree_node_ids[subtree_node_ids %in% df$parent]
subtree_node_ids = subtree_node_ids[subtree_node_ids != origin_id]
for(parent_id in subtree_node_ids){
|
40f0f078 |
# Get angle from origin node to parent node.
|
fc03966c |
theta_parent <- getNodeAngle.vector(x_origin, y_origin, df_x[parent_id], df_y[parent_id])
|
a0450f48 |
## children_ids <- getChild.df(df, parent_id)
|
aff471e1 |
children_ids <- tidytree:::child.tbl_tree(df, parent_id)$node
|
fc03966c |
# Skip if child is parent node of subtree.
children_ids = children_ids[children_ids != origin_id]
for(child_id in children_ids){
theta_child <- getNodeAngle.vector(x_origin, y_origin, df_x[child_id], df_y[child_id])
|
40f0f078 |
# Skip if child node is already inside arc.
# if left < right angle (arc crosses 180/-180 quadrant) and child node is not inside arc of tree.
# OR if left > right angle (arc crosses 0/360 quadrant) and child node is inside gap
|
fc03966c |
if ((arc['left'] < arc['right'] & !(theta_child > arc['left'] & theta_child < arc['right'])) |
(arc['left'] > arc['right'] & (theta_child < arc['left'] & theta_child > arc['right'])) ){
|
40f0f078 |
# child node inside arc.
next
}
delta <- theta_child - theta_parent
delta_adj <- delta
# Correct the delta if parent and child angles cross the 180/-180 half of circle.
# If delta > 180
if( delta > 1){ # Edge between parent and child cross upper and lower quadrants of cirlce on 180/-180 side.
delta_adj <- delta - 2 # delta' = delta - 360
# If delta < -180
}else if( delta < -1){ # Edge between parent and child cross upper and lower quadrants of cirlce
delta_adj <- delta + 2 # delta' = delta - 360
}
theta_child_adj <- theta_child
# If angle change from parent to node is positive (anti-clockwise), check left angle
if(delta_adj > 0){
|
a6e5cc92 |
# If child/parent edges cross the -180/180 quadrant (angle between them is > 180),
|
40f0f078 |
# check if right angle and child angle are different signs and adjust if needed.
if( abs(delta) > 1){
if( arc['left'] > 0 & theta_child < 0){
theta_child_adj <- theta_child + 2
}else if (arc['left'] < 0 & theta_child > 0){
theta_child_adj <- theta_child - 2
}
}
|
fc03966c |
# check if left angle of arc is less than angle of child. Update if true.
|
40f0f078 |
if( arc['left'] < theta_child_adj ){
arc['left'] <- theta_child
|
a76f35c1 |
}
|
a6e5cc92 |
# If angle change from parent to node is negative (clockwise), check right angle
|
40f0f078 |
}else if(delta_adj < 0){
|
a6e5cc92 |
# If child/parent edges cross the -180/180 quadrant (angle between them is > 180),
|
40f0f078 |
# check if right angle and child angle are different signs and adjust if needed.
if( abs(delta) > 1){
# Else change in angle from parent to child is negative, then adjust child angle if right angle is a different sign.
if( arc['right'] > 0 & theta_child < 0){
theta_child_adj <- theta_child + 2
}else if (arc['right'] < 0 & theta_child > 0){
theta_child_adj <- theta_child - 2
}
}
# check if right angle of arc is greater than angle of child. Update if true.
if( arc['right'] > theta_child_adj ){
arc['right'] <- theta_child
|
a6e5cc92 |
}
|
40f0f078 |
}
}
|
a6e5cc92 |
}
|
40f0f078 |
# Convert arc angles of [1, -1] to [2,0] domain.
arc[arc<0] <- arc[arc<0] + 2
|
fc03966c |
arc
|
40f0f078 |
}
|
a76f35c1 |
|
40f0f078 |
##' Rotate the points in a tree data.frame around a pivot node by the angle specified.
|
a6e5cc92 |
##'
|
1540db03 |
##' @title rotateTreePoints.data.frame
##' @rdname rotateTreePoints
|
40f0f078 |
##' @param df tree data.frame
##' @param pivot_node is the id of the pivot node.
##' @param nodes list of node numbers that are to be rotated by angle around the pivot_node
|
1540db03 |
##' @param angle in range `[0,2]`, ie degrees/180, radians/pi
|
40f0f078 |
##' @return updated tree data.frame with points rotated by angle
rotateTreePoints.df <- function(df, pivot_node, nodes, angle){
# Rotate nodes around pivot_node.
# x' = cos(angle)*delta_x - sin(angle)*delta_y + delta_x
# y' = sin(angle)*delta_x + cos(angle)*delta_y + delta_y
cospitheta <- cospi(angle)
sinpitheta <- sinpi(angle)
|
2fa23352 |
pivot_x = df$x[pivot_node]
pivot_y = df$y[pivot_node]
|
fc03966c |
delta_x = df$x - pivot_x
delta_y = df$y - pivot_y
|
80b1a5bb |
df = mutate(df,
|
fc03966c |
x = ifelse(.data$node %in% nodes, cospitheta * delta_x - sinpitheta * delta_y + pivot_x, .data$x),
y = ifelse(.data$node %in% nodes, sinpitheta * delta_x + cospitheta * delta_y + pivot_y, .data$y)
|
2fa23352 |
)
|
fc03966c |
x_parent = df$x[df$parent]
y_parent = df$y[df$parent]
|
1cfeeff1 |
# Now update tip labels of rotated tree.
# angle is in range [0, 360]
|
2fa23352 |
# Update label angle of tipnode if not root node.
nodes = nodes[! nodes %in% df$parent]
|
80b1a5bb |
df %>% mutate(
|
fc03966c |
angle = ifelse(.data$node %in% nodes,
getNodeAngle.vector(x_parent, y_parent, .data$x, .data$y) %>%
{180 * ifelse(. < 0, 2 + ., .)},
.data$angle)
)
|
40f0f078 |
}
|
a76f35c1 |
|
40f0f078 |
##' Get the angle between the two nodes specified.
|
a6e5cc92 |
##'
|
40f0f078 |
##' @title getNodeAngle.df
##' @param df tree data.frame
##' @param origin_node_id origin node id number
##' @param node_id end node id number
|
1540db03 |
##' @return angle in range `[-1, 1]`, i.e. degrees/180, radians/pi
|
40f0f078 |
getNodeAngle.df <- function(df, origin_node_id, node_id){
|
2fa23352 |
if (origin_node_id != node_id) {
|
fc03966c |
df_x = df$x
df_y = df$y
atan2(df_y[node_id] - df_y[origin_node_id], df_x[node_id] - df_x[origin_node_id]) / pi
|
40f0f078 |
}else{
|
fc03966c |
NA
|
40f0f078 |
}
}
|
2d02a2e0 |
|
fc03966c |
getNodeAngle.vector <- function(x_origin, y_origin, x, y) {
atan2(y - y_origin, x - x_origin) / pi
}
|
9a1c4d17 |
euc.dist <- function(x1, x2) sqrt(sum((x1 - x2) ^ 2))
|
96532506 |
## Get the distances from the node to all other nodes in data.frame (including itself if in df)
|
9a1c4d17 |
getNodeEuclDistances <- function(df, node){
# https://stackoverflow.com/questions/24746892/how-to-calculate-euclidian-distance-between-two-points-defined-by-matrix-contain#24747155
dist <- NULL
for(i in 1:nrow(df)) dist[i] <- euc.dist(df[df$node==node, c('x', 'y')], df[i, c('x', 'y')])
return(dist)
}
|
a76f35c1 |
|
40f0f078 |
##' Get all children of node from tree, including start_node.
|
a6e5cc92 |
##'
|
40f0f078 |
##' @title getSubtree
##' @param tree ape phylo tree object
##' @param node is the tree node id from which the tree is derived.
##' @return list of all child node id's from starting node.
getSubtree <- function(tree, node){
|
a6e5cc92 |
|
a0450f48 |
## subtree <- c(node)
## i <- 1
## while( i <= length(subtree)){
## subtree <- c(subtree, treeio::child(tree, subtree[i]))
## # remove any '0' root nodes
## subtree <- subtree[subtree != 0]
## i <- i + 1
## }
## return(subtree)
tidytree::offspring(tree, node, self_include = TRUE)
|
a76f35c1 |
}
|
a6e5cc92 |
##' Get all children of node from df tree using breath-first.
##'
|
9a1c4d17 |
##' @title getSubtree.df
|
40f0f078 |
##' @param df tree data.frame
##' @param node id of starting node.
##' @return list of all child node id's from starting node.
|
9a1c4d17 |
getSubtree.df <- function(df, node){
|
a0450f48 |
## subtree <- node[node != 0]
## i <- 1
## while( i <= length(subtree)){
## ## subtree <- c(subtree, getChild.df(df, subtree[i]))
## subtree <- c(subtree, tidytree::child(df, subtree[i])$node)
## i <- i + 1
## }
## subtree
|
13d5b831 |
#tidytree:::offspring.tbl_tree(df, node, self_include = TRUE)$node
offspring.tbl_tree(df, node, self_include = TRUE)$node
|
40f0f078 |
}
|
a6e5cc92 |
##' Get all subtrees of specified node. This includes all ancestors and relatives of node and
|
40f0f078 |
##' return named list of subtrees.
|
a6e5cc92 |
##'
|
9a1c4d17 |
##' @title getSubtreeUnrooted
|
40f0f078 |
##' @param tree ape phylo tree object
##' @param node is the tree node id from which the subtrees are derived.
##' @return named list of subtrees with the root id of subtree and list of node id's making up subtree.
|
9a1c4d17 |
getSubtreeUnrooted <- function(tree, node){
|
40f0f078 |
# if node leaf, return nothing.
|
a0450f48 |
if( treeio::isTip(tree, node) ){
|
40f0f078 |
# return NA
return(NA)
}
|
a6e5cc92 |
|
40f0f078 |
subtrees <- list()
|
a6e5cc92 |
|
40f0f078 |
# get subtree for each child node.
|
a0450f48 |
## children_ids <- getChild(tree, node)
children_ids <- treeio::child(tree, node)
|
a6e5cc92 |
|
40f0f078 |
remaining_nodes <- getNodes_by_postorder(tree)
# Remove current node from remaining_nodes list.
remaining_nodes <- setdiff(remaining_nodes, node)
|
a6e5cc92 |
|
40f0f078 |
for( child in children_ids ){
# Append subtree nodes to list if not 0 (root).
subtree <- getSubtree(tree, child)
subtrees[[length(subtrees)+1]] <- list( node = child, subtree = subtree)
# remove subtree nodes from remaining nodes.
remaining_nodes <- setdiff(remaining_nodes, as.integer(unlist(subtrees[[length(subtrees)]]['subtree']) ))
}
|
a6e5cc92 |
|
40f0f078 |
# The remaining nodes that are not found in the child subtrees are the remaining subtree nodes.
# ie, parent node and all other nodes. We don't care how they are connect, just their ids.
|
a0450f48 |
parent_id <- parent(tree, node)
|
40f0f078 |
# If node is not root, add remainder of tree nodes as subtree.
if( parent_id != 0 & length(remaining_nodes) >= 1){
subtrees[[length(subtrees)+1]] <- list( node = parent_id, subtree = remaining_nodes)
}
|
a6e5cc92 |
|
40f0f078 |
return(subtrees)
}
##' Get all subtrees of node, as well as remaining branches of parent (ie, rest of tree structure as subtree)
##' return named list of subtrees with list name as starting node id.
|
9a1c4d17 |
##' @title getSubtreeUnrooted
|
40f0f078 |
##' @param df tree data.frame
##' @param node is the tree node id from which the subtrees are derived.
|
a0450f48 |
##' @importFrom tidytree parent
|
40f0f078 |
##' @return named list of subtrees with the root id of subtree and list of node id's making up subtree.
getSubtreeUnrooted.df <- function(df, node){
# get subtree for each child node.
|
a0450f48 |
# children_ids <- getChild.df(df, node)
|
54a04a60 |
children_ids <- child.tbl_tree(df, node)$node
|
2fa23352 |
if (length(children_ids) == 0L) return(NULL)
# if node leaf, return nothing.
|
a6e5cc92 |
|
fc03966c |
subtrees = tibble::tibble(
node = children_ids,
subtree = purrr::map(.data$node, ~getSubtree.df(df, .x))
)
remaining_nodes = setdiff(df$node, purrr::flatten_int(subtrees$subtree))
|
a6e5cc92 |
|
40f0f078 |
# The remaining nodes that are not found in the child subtrees are the remaining subtree nodes.
# ie, parent node and all other nodes. We don't care how they are connected, just their id.
|
76f68aee |
parent_id <- parent.tbl_tree(df, node)$node
|
40f0f078 |
# If node is not root.
|
fc03966c |
if ((length(parent_id) > 0) & (length(remaining_nodes) > 0)) {
subtrees = tibble::add_row(subtrees, node = parent_id, subtree = list(remaining_nodes))
|
40f0f078 |
}
|
fc03966c |
purrr::transpose(subtrees)
|
40f0f078 |
}
getRoot.df <- function(df, node){
|
96532506 |
|
40f0f078 |
root <- which(is.na(df$parent))
|
9a1c4d17 |
# Check if root was found.
if(length(root) == 0){
|
b681f0b4 |
## Alternatively, root can self reference, eg node = 10, parent = 10
root <- df$node[df$parent == df$node]
## root <- unlist(apply(df, 1, function(x){ if(x['node'] == x['parent']){ x['node'] } }))
|
9a1c4d17 |
}
|
40f0f078 |
return(root)
}
##' Get the nodes of tree from root in breadth-first order.
|
a6e5cc92 |
##'
|
40f0f078 |
##' @title getNodesBreadthFirst.df
##' @param df tree data.frame
##' @return list of node id's in breadth-first order.
getNodesBreadthFirst.df <- function(df){
root <- getRoot.df(df)
|
a0450f48 |
if(treeio::isTip(df, root)){
|
40f0f078 |
return(root)
}
tree_size <- nrow(df)
# initialise list of nodes
res <- root
|
a6e5cc92 |
|
40f0f078 |
i <- 1
while(length(res) < tree_size){
parent <- res[i]
i <- i + 1
|
a6e5cc92 |
|
40f0f078 |
# Skip if parent is a tip.
|
a0450f48 |
if(treeio::isTip(df, parent)){
|
40f0f078 |
next
}
|
a6e5cc92 |
|
40f0f078 |
# get children of current parent.
|
a0450f48 |
children <- tidytree::child(df,parent)$node
|
40f0f078 |
# add children to result
res <- c(res, children)
|
a6e5cc92 |
|
40f0f078 |
}
|
a6e5cc92 |
|
40f0f078 |
return(res)
|
a6e5cc92 |
|
40f0f078 |
}
|
99048963 |
isRoot <- function(tr, node) {
getRoot(tr) == node
}
getNodeName <- function(tr) {
if (is.null(tr$node.label)) {
n <- length(tr$tip.label)
nl <- (n + 1):(2 * n - 2)
nl <- as.character(nl)
}
else {
nl <- tr$node.label
}
nodeName <- c(tr$tip.label, nl)
return(nodeName)
}
get.trunk <- function(tr) {
root <- getRoot(tr)
path_length <- sapply(1:(root-1), function(x) get.path_length(tr, root, x))
i <- which.max(path_length)
return(get.path(tr, root, i))
}
##' path from start node to end node
##'
##'
##' @title get.path
##' @param phylo phylo object
##' @param from start node
##' @param to end node
##' @return node vectot
|
a0450f48 |
##' @importFrom tidytree ancestor
|
99048963 |
##' @export
##' @author Guangchuang Yu
get.path <- function(phylo, from, to) {
|
a0450f48 |
anc_from <- ancestor(phylo, from)
|
99048963 |
anc_from <- c(from, anc_from)
|
a0450f48 |
anc_to <- ancestor(phylo, to)
|
99048963 |
anc_to <- c(to, anc_to)
mrca <- intersect(anc_from, anc_to)[1]
i <- which(anc_from == mrca)
j <- which(anc_to == mrca)
path <- c(anc_from[1:i], rev(anc_to[1:(j-1)]))
return(path)
}
get.path_length <- function(phylo, from, to, weight=NULL) {
path <- get.path(phylo, from, to)
if (is.null(weight)) {
return(length(path)-1)
}
df <- fortify(phylo)
if ( ! (weight %in% colnames(df))) {
stop("weight should be one of numerical attributes of the tree...")
}
res <- 0
get_edge_index <- function(df, from, to) {
which((df[,1] == from | df[,2] == from) &
(df[,1] == to | df[,2] == to))
}
for(i in 1:(length(path)-1)) {
ee <- get_edge_index(df, path[i], path[i+1])
res <- res + df[ee, weight]
}
return(res)
}
##' @importFrom ape reorder.phylo
getNodes_by_postorder <- function(tree) {
tree <- reorder.phylo(tree, "postorder")
unique(rev(as.vector(t(tree$edge[,c(2,1)]))))
}
getXcoord2 <- function(x, root, parent, child, len, start=0, rev=FALSE) {
x[root] <- start
x[-root] <- NA ## only root is set to start, by default 0
currentNode <- root
direction <- 1
if (rev == TRUE) {
direction <- -1
}
|
17e4ed93 |
ignore_negative_edge <- getOption("ignore.negative.edge", default=FALSE)
if (any(len < 0) && !ignore_negative_edge) {
|
1a727315 |
warning_wrap("The tree contained negative ",
|
17e4ed93 |
ifelse(sum(len < 0)>1, "edge lengths", "edge length"),
". If you want to ignore the ",
ifelse(sum(len<0) > 1, "edges", "edge"),
", you can set 'options(ignore.negative.edge=TRUE)', then re-run ggtree."
)
|
99048963 |
}
|
17e4ed93 |
while(anyNA(x)) {
idx <- which(parent %in% currentNode)
newNode <- child[idx]
if (ignore_negative_edge){
|
1a727315 |
x[newNode] <- x[parent[idx]]+len[idx] * direction * sign(len[idx])
|
17e4ed93 |
} else {
|
1a727315 |
x[newNode] <- x[parent[idx]]+len[idx] * direction
|
17e4ed93 |
}
currentNode <- newNode
|
1a727315 |
}
|
99048963 |
return(x)
}
getXcoord_no_length <- function(tr) {
edge <- tr$edge
parent <- edge[,1]
child <- edge[,2]
root <- getRoot(tr)
len <- tr$edge.length
N <- getNodeNum(tr)
x <- numeric(N)
ntip <- Ntip(tr)
currentNode <- 1:ntip
x[-currentNode] <- NA
cl <- split(child, parent)
child_list <- list()
child_list[as.numeric(names(cl))] <- cl
while(anyNA(x)) {
idx <- match(currentNode, child)
pNode <- parent[idx]
## child number table
p1 <- table(parent[parent %in% pNode])
p2 <- table(pNode)
np <- names(p2)
i <- p1[np] == p2
newNode <- as.numeric(np[i])
exclude <- rep(NA, max(child))
for (j in newNode) {
x[j] <- min(x[child_list[[j]]]) - 1
exclude[child_list[[j]]] <- child_list[[j]]
}
exclude <- exclude[!is.na(exclude)]
## currentNode %<>% `[`(!(. %in% exclude))
## currentNode %<>% c(., newNode) %>% unique
currentNode <- currentNode[!currentNode %in% exclude]
currentNode <- unique(c(currentNode, newNode))
}
x <- x - min(x)
return(x)
}
getXcoord <- function(tr) {
edge <- tr$edge
parent <- edge[,1]
child <- edge[,2]
root <- getRoot(tr)
len <- tr$edge.length
N <- getNodeNum(tr)
x <- numeric(N)
x <- getXcoord2(x, root, parent, child, len)
return(x)
}
## scale the branch (the line plotted) to the actual value of edge length
## but it seems not the good idea as if we want to add x-axis (e.g. time-scaled tree)
## then the x-value is not corresponding to edge length as in rectangular layout
## getXYcoord_slanted <- function(tr) {
## edge <- tr$edge
## parent <- edge[,1]
## child <- edge[,2]
## root <- getRoot(tr)
## N <- getNodeNum(tr)
## len <- tr$edge.length
## y <- getYcoord(tr, step=min(len)/2)
## len <- sqrt(len^2 - (y[parent]-y[child])^2)
## x <- numeric(N)
## x <- getXcoord2(x, root, parent, child, len)
## res <- data.frame(x=x, y=y)
## return(res)
## }
## @importFrom magrittr %>%
##' @importFrom magrittr equals
|
87cb27be |
getYcoord <- function(tr, step=1, tip.order = NULL) {
|
99048963 |
Ntip <- length(tr[["tip.label"]])
N <- getNodeNum(tr)
edge <- tr[["edge"]]
parent <- edge[,1]
child <- edge[,2]
cl <- split(child, parent)
child_list <- list()
child_list[as.numeric(names(cl))] <- cl
y <- numeric(N)
|
87cb27be |
if (is.null(tip.order)) {
tip.idx <- child[child <= Ntip]
y[tip.idx] <- 1:Ntip * step
} else {
tip.idx <- 1:Ntip
y[tip.idx] <- match(tr$tip.label, tip.order) * step
}
|
99048963 |
y[-tip.idx] <- NA
|
80b1a5bb |
|
99048963 |
## use lookup table
pvec <- integer(max(tr$edge))
pvec[child] = parent
currentNode <- 1:Ntip
while(anyNA(y)) {
## pNode <- unique(parent[child %in% currentNode])
pNode <- unique(pvec[currentNode])
## piping of magrittr is slower than nested function call.
## pipeR is fastest, may consider to use pipeR
##
## child %in% currentNode %>% which %>% parent[.] %>% unique
## idx <- sapply(pNode, function(i) all(child[parent == i] %in% currentNode))
idx <- sapply(pNode, function(i) all(child_list[[i]] %in% currentNode))
newNode <- pNode[idx]
y[newNode] <- sapply(newNode, function(i) {
mean(y[child_list[[i]]], na.rm=TRUE)
##child[parent == i] %>% y[.] %>% mean(na.rm=TRUE)
})
currentNode <- c(currentNode[!currentNode %in% unlist(child_list[newNode])], newNode)
## currentNode <- c(currentNode[!currentNode %in% child[parent %in% newNode]], newNode)
## parent %in% newNode %>% child[.] %>%
## `%in%`(currentNode, .) %>% `!` %>%
## currentNode[.] %>% c(., newNode)
}
return(y)
}
getYcoord_scale <- function(tr, df, yscale) {
N <- getNodeNum(tr)
y <- numeric(N)
root <- getRoot(tr)
y[root] <- 0
y[-root] <- NA
edge <- tr$edge
parent <- edge[,1]
child <- edge[,2]
currentNodes <- root
while(anyNA(y)) {
newNodes <- c()
for (currentNode in currentNodes) {
idx <- which(parent %in% currentNode)
newNode <- child[idx]
direction <- -1
for (i in seq_along(newNode)) {
y[newNode[i]] <- y[currentNode] + df[newNode[i], yscale] * direction
direction <- -1 * direction
}
newNodes <- c(newNodes, newNode)
}
currentNodes <- unique(newNodes)
}
if (min(y) < 0) {
y <- y + abs(min(y))
}
return(y)
}
getYcoord_scale2 <- function(tr, df, yscale) {
root <- getRoot(tr)
pathLength <- sapply(1:length(tr$tip.label), function(i) {
get.path_length(tr, i, root, yscale)
})
ordered_tip <- order(pathLength, decreasing = TRUE)
ii <- 1
ntip <- length(ordered_tip)
while(ii < ntip) {
|
a0450f48 |
sib <- tidytree::sibling(tr, ordered_tip[ii])
|
99048963 |
if (length(sib) == 0) {
ii <- ii + 1
next
}
jj <- which(ordered_tip %in% sib)
if (length(jj) == 0) {
ii <- ii + 1
next
}
sib <- ordered_tip[jj]
ordered_tip <- ordered_tip[-jj]
nn <- length(sib)
if (ii < length(ordered_tip)) {
ordered_tip <- c(ordered_tip[1:ii],sib, ordered_tip[(ii+1):length(ordered_tip)])
} else {
ordered_tip <- c(ordered_tip[1:ii],sib)
}
ii <- ii + nn + 1
}
|
a0450f48 |
long_branch <- ancestor(tr, ordered_tip[1]) %>% rev
|
99048963 |
long_branch <- c(long_branch, ordered_tip[1])
N <- getNodeNum(tr)
y <- numeric(N)
y[root] <- 0
y[-root] <- NA
## yy <- df[, yscale]
## yy[is.na(yy)] <- 0
for (i in 2:length(long_branch)) {
y[long_branch[i]] <- y[long_branch[i-1]] + df[long_branch[i], yscale]
}
parent <- df[, "parent"]
child <- df[, "node"]
currentNodes <- root
while(anyNA(y)) {
newNodes <- c()
for (currentNode in currentNodes) {
idx <- which(parent %in% currentNode)
newNode <- child[idx]
newNode <- c(newNode[! newNode %in% ordered_tip],
rev(ordered_tip[ordered_tip %in% newNode]))
direction <- -1
for (i in seq_along(newNode)) {
if (is.na(y[newNode[i]])) {
y[newNode[i]] <- y[currentNode] + df[newNode[i], yscale] * direction
direction <- -1 * direction
}
}
newNodes <- c(newNodes, newNode)
}
currentNodes <- unique(newNodes)
}
if (min(y) < 0) {
y <- y + abs(min(y))
}
return(y)
}
getYcoord_scale_numeric <- function(tr, df, yscale, ...) {
df <- .assign_parent_status(tr, df, yscale)
df <- .assign_child_status(tr, df, yscale)
y <- df[, yscale]
if (anyNA(y)) {
warning("NA found in y scale mapping, all were setting to 0")
y[is.na(y)] <- 0
}
return(y)
}
.assign_parent_status <- function(tr, df, variable) {
yy <- df[[variable]]
na.idx <- which(is.na(yy))
if (length(na.idx) > 0) {
tree <- get.tree(tr)
nodes <- getNodes_by_postorder(tree)
for (curNode in nodes) {
|
a0450f48 |
children <- treeio::child(tree, curNode)
|
99048963 |
if (length(children) == 0) {
next
}
idx <- which(is.na(yy[children]))
if (length(idx) > 0) {
yy[children[idx]] <- yy[curNode]
}
}
}
df[, variable] <- yy
return(df)
}
.assign_child_status <- function(tr, df, variable, yscale_mapping=NULL) {
yy <- df[[variable]]
if (!is.null(yscale_mapping)) {
yy <- yscale_mapping[yy]
}
na.idx <- which(is.na(yy))
if (length(na.idx) > 0) {
tree <- get.tree(tr)
nodes <- rev(getNodes_by_postorder(tree))
for (curNode in nodes) {
|
a0450f48 |
parent <- parent(tree, curNode)
|
99048963 |
if (parent == 0) { ## already reach root
next
}
idx <- which(is.na(yy[parent]))
if (length(idx) > 0) {
|
a0450f48 |
child <- treeio::child(tree, parent)
|
99048963 |
yy[parent[idx]] <- mean(yy[child], na.rm=TRUE)
}
}
}
df[, variable] <- yy
return(df)
}
getYcoord_scale_category <- function(tr, df, yscale, yscale_mapping=NULL, ...) {
if (is.null(yscale_mapping)) {
stop("yscale is category variable, user should provide yscale_mapping,
which is a named vector, to convert yscale to numberical values...")
}
if (! is(yscale_mapping, "numeric") ||
is.null(names(yscale_mapping))) {
stop("yscale_mapping should be a named numeric vector...")
}
if (yscale == "label") {
yy <- df[[yscale]]
ii <- which(is.na(yy))
if (length(ii)) {
|
ecf2419f |
## df[ii, yscale] <- df[ii, "node"]
df[[yscale]][ii] <- as.character(df[['node']][ii])
|
99048963 |
}
}
## assign to parent status is more prefer...
df <- .assign_parent_status(tr, df, yscale)
df <- .assign_child_status(tr, df, yscale, yscale_mapping)
y <- df[[yscale]]
if (anyNA(y)) {
warning("NA found in y scale mapping, all were setting to 0")
y[is.na(y)] <- 0
}
return(y)
}
add_angle_slanted <- function(res) {
x <- res[["x"]]
y <- res[["y"]]
dy <- (y - y[match(res$parent, res$node)]) / diff(range(y))
dx <- (x - x[match(res$parent, res$node)]) / diff(range(x))
theta <- atan(dy/dx)
theta[is.na(theta)] <- 0 ## root node
res$angle <- theta/pi * 180
branch.y <- (y[match(res$parent, res$node)] + y)/2
idx <- is.na(branch.y)
branch.y[idx] <- y[idx]
res[, "branch.y"] <- branch.y
return(res)
}
|
64fcaa9e |
calculate_branch_mid <- function(res, layout) {
if (layout %in% c("equal_angle", "daylight", "ape")){
res$branch.y <- with(res, (y[match(parent, node)] + y)/2)
res$branch.y[is.na(res$branch.y)] <- 0
}
|
99048963 |
res$branch <- with(res, (x[match(parent, node)] + x)/2)
|
149acb32 |
if (!is.null(res[['branch.length']])) {
|
99048963 |
res$branch.length[is.na(res$branch.length)] <- 0
}
res$branch[is.na(res$branch)] <- 0
|
64fcaa9e |
if (layout %in% c("equal_angle", "daylight", "ape")){
res$branch.x <- res$branch
}
|
99048963 |
return(res)
}
re_assign_ycoord_df <- function(df, currentNode) {
while(anyNA(df$y)) {
pNode <- with(df, parent[match(currentNode, node)]) %>% unique
idx <- sapply(pNode, function(i) with(df, all(node[parent == i & parent != node] %in% currentNode)))
newNode <- pNode[idx]
## newNode <- newNode[is.na(df[match(newNode, df$node), "y"])]
|
cf8b3bfe |
if (length(newNode) == 0)
break
|
99048963 |
df[match(newNode, df$node), "y"] <- sapply(newNode, function(i) {
with(df, mean(y[parent == i], na.rm = TRUE))
})
traced_node <- as.vector(sapply(newNode, function(i) with(df, node[parent == i])))
currentNode <- c(currentNode[! currentNode %in% traced_node], newNode)
}
return(df)
}
|
ceb13aec |
|
b8275604 |
layoutApe <- function(model, branch.length="branch.length") {
|
ceb13aec |
tree <- as.phylo(model) %>% stats::reorder("postorder")
|
80b1a5bb |
|
642f657b |
if (! is.null(tree$edge.length)) {
if (anyNA(tree$edge.length)) {
warning("'edge.length' contains NA values...\n## setting 'edge.length' to NULL automatically when plotting the tree...")
tree$edge.length <- NULL
}
}
|
80b1a5bb |
|
642f657b |
if (is.null(tree$edge.length) || branch.length == "none") {
tree <- set_branch_length_cladogram(tree)
}
|
80b1a5bb |
|
b8275604 |
edge <- tree$edge
edge.length <- tree$edge.length
nb.sp <- ape::node.depth(tree)
|
80b1a5bb |
|
b8275604 |
df <- as_tibble(model) %>%
|
f3952913 |
mutate(isTip = ! .data$node %in% .data$parent)
|
f4a7ca94 |
#df$branch.length <- edge.length[df$node] # for cladogram
|
80b1a5bb |
|
642f657b |
# unrooted layout from cran/ape
M <- ape::unrooted.xy(Ntip(tree),
Nnode(tree),
tree$edge,
tree$edge.length,
nb.sp,
0)$M
xx <- M[, 1]
yy <- M[, 2]
|
80b1a5bb |
|
53ab3069 |
M <- tibble::tibble(
|
b8275604 |
node = 1:(Ntip(tree) + Nnode(tree)),
x = xx - min(xx),
|
642f657b |
y = yy - min(yy)
|
b8275604 |
)
|
80b1a5bb |
|
b8275604 |
tree_df <- dplyr::full_join(df, M, by = "node") %>%
as_tibble()
class(tree_df) <- c("tbl_tree", class(tree_df))
tree_df
}
|