diff --git a/NEWS.md b/NEWS.md index 9164413..bd74d1b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -9,6 +9,7 @@ * Node-by-node control of node size * Aesthetic changes for heatmap and clone-genome distribution plot * Add parameters to specify polygon shape and width. +* Add option to use scale bars instead of y-axes. * Wrapper function for `SRCgrob` to automatically save plots to file ## Update diff --git a/R/SRCGrob.R b/R/SRCGrob.R index 2b4361e..9cd46b3 100644 --- a/R/SRCGrob.R +++ b/R/SRCGrob.R @@ -27,7 +27,12 @@ SRCGrob <- function( polygon.shape = 3, polygon.width = 1.2, length.from.node.edge = TRUE, - size.units = 'npc' + size.units = 'npc', + scale.bar = FALSE, + scale.bar.coords = c(0.5, 1), + scale.size.1 = NA, + scale.size.2 = NA, + scale.padding = 1 ) { add.node.text <- !is.null(node.text); @@ -110,7 +115,12 @@ SRCGrob <- function( main = main, main.cex = main.cex, main.y = main.y, - size.units = size.units + size.units = size.units, + scale.bar = scale.bar, + scale.bar.coords = scale.bar.coords, + scale.size.1 = scale.size.1, + scale.size.2 = scale.size.2, + scale.padding = scale.padding ); out.tree <- gTree( diff --git a/R/calculate.clone.polygons.R b/R/calculate.clone.polygons.R index 1caed88..7ef9061 100644 --- a/R/calculate.clone.polygons.R +++ b/R/calculate.clone.polygons.R @@ -331,7 +331,6 @@ compute.clones <- function( v <- v[is.na(v$parent) | v$parent != -1, ]; v <- rbind(root, v); v <- count.leaves.per.node(v); - if (no.ccf) { tree$angle <- if ((is.null(fixed.angle) && nrow(v) > 6) || any(table(v$parent) > 2)) { tau <- -(pi / 2.5); diff --git a/R/make.clone.tree.grobs.R b/R/make.clone.tree.grobs.R index 0ccee07..ec4d8ea 100644 --- a/R/make.clone.tree.grobs.R +++ b/R/make.clone.tree.grobs.R @@ -37,42 +37,47 @@ make.clone.tree.grobs <- function( main.cex, main.y, size.units, + scale.bar, + scale.bar.coords, + scale.size.1, + scale.size.2, + scale.padding, ... ) { - #initializing dataframe for subclones - if ('excluded' %in% colnames(ccf.df)) { - v <- ccf.df[!ccf.df$excluded,]; - } else { - v <- ccf.df; - v$excluded <- FALSE; - } - - v <- v[order(v$id), ]; - no.ccf <- FALSE; - - if (!('ccf' %in% colnames(ccf.df)) || all(is.na(ccf.df$ccf)) || add.polygons == FALSE) { - v$vaf <- NULL; - v$vaf[v$parent == -1] <- 1; - no.ccf <- TRUE; - } else { - v <- v[order(v$id),] - v$vaf[!v$excluded] <- v$ccf[!v$excluded] / max(v$ccf[!v$excluded]); - } - - if (all(is.null(ccf.df$colour))) { - v$colour <- node.col - } + #initializing dataframe for subclones + if ('excluded' %in% colnames(ccf.df)) { + v <- ccf.df[!ccf.df$excluded,]; + } else { + v <- ccf.df; + v$excluded <- FALSE; + } + + v <- v[order(v$id), ]; + no.ccf <- FALSE; + + if (!('ccf' %in% colnames(ccf.df)) || all(is.na(ccf.df$ccf)) || add.polygons == FALSE) { + v$vaf <- NULL; + v$vaf[v$parent == -1] <- 1; + no.ccf <- TRUE; + } else { + v <- v[order(v$id),] + v$vaf[!v$excluded] <- v$ccf[!v$excluded] / max(v$ccf[!v$excluded]); + } + + if (all(is.null(ccf.df$colour))) { + v$colour <- node.col + } extra.len <- if (no.ccf) node.radius else node.radius * 4; - v$x <- v$y <- v$len <- v$x.mid <- numeric(length(nrow(v))); - v <- v[order(v$tier, v$parent), ]; + v$x <- v$y <- v$len <- v$x.mid <- numeric(length(nrow(v))); + v <- v[order(v$tier, v$parent), ]; - #initializing line segment dataframe and adjusting lengths to accomodate the node circles - tree$angle <- numeric(length = nrow(tree)); - tree$angle[tree$parent == -1] <- 0; - if ('length2' %in% colnames(tree)) { + #initializing line segment dataframe and adjusting lengths to accomodate the node circles + tree$angle <- numeric(length = nrow(tree)); + tree$angle[tree$parent == -1] <- 0; + if ('length2' %in% colnames(tree)) { tree$length2.c <- tree$length2 / scale1 * scale2; tree$length <- apply( @@ -82,103 +87,124 @@ make.clone.tree.grobs <- function( max(x[c(3, 6)]); } ); - } else { - tree$length <- tree$length1; - } - - if (length.from.node.edge == TRUE) { - tree <- adjust.branch.lengths(v, tree, node.radius, scale1); - } - - extra.len <- extra.len * (1 / scale1); - - clone.out <- make.clone.polygons( - v, - tree, - wid, - scale1, - scale2, - extra.len, - node.col, - spread = spread, - sig.shape = sig.shape, - fixed.angle = fixed.angle, - add.polygons = add.polygons, - no.ccf = no.ccf - ); - - clone.out$no.ccf <- no.ccf; - plot.size <- calculate.main.plot.size( - clone.out, - scale1, - wid, - min.width, - node.radius - ); - - if (!no.ccf) { - get.CP.polygons(clone.out); - } - - add.tree.segs(clone.out, node.radius, default.branch.width, scale1, seg1.col, seg2.col); - - if (!is.null(cluster.list)) { - message(paste( - 'Clustered pie nodes will be supported in a future version.', - 'Plain nodes will be used.' - )); - # TODO Implement pie nodes - # add.pie.nodes(clone.out, node.radius, cluster.list); - } + } else { + tree$length <- tree$length1; + } + + if (length.from.node.edge == TRUE) { + tree <- adjust.branch.lengths(v, tree, node.radius, scale1); + } + + extra.len <- extra.len * (1 / scale1); + + clone.out <- make.clone.polygons( + v, + tree, + wid, + scale1, + scale2, + extra.len, + node.col, + spread = spread, + sig.shape = sig.shape, + fixed.angle = fixed.angle, + add.polygons = add.polygons, + no.ccf = no.ccf + ); + + clone.out$no.ccf <- no.ccf; + plot.size <- calculate.main.plot.size( + clone.out, + scale1, + wid, + min.width, + node.radius + ); + + if (!no.ccf) { + get.CP.polygons(clone.out); + } + + add.tree.segs(clone.out, node.radius, default.branch.width, scale1, seg1.col, seg2.col); + + if (!is.null(cluster.list)) { + message(paste( + 'Clustered pie nodes will be supported in a future version.', + 'Plain nodes will be used.' + )); + # TODO Implement pie nodes + # add.pie.nodes(clone.out, node.radius, cluster.list); + } add.node.ellipse(clone.out,node.radius, label.nodes, label.cex, scale1); - if (add.normal == TRUE) { - add.normal(clone.out,node.radius,label.cex, normal.cex) - } - - if (yaxis.position != 'none') { - add.axes( - clone.out, - yaxis.position, - scale1 = scale1, - scale2 = scale2, - yat = yat, - axis.label.cex = axis.label.cex, - axis.cex = axis.cex, - no.ccf = no.ccf, - xaxis.label = xaxis.label, - yaxis1.label = yaxis1.label, - yaxis2.label = yaxis2.label - ); - } - - if (add.node.text == TRUE & !is.null(text.df)) { - node.text.grobs <- add.text2( - clone.out$tree, - text.df, - label.nodes = text.on.nodes, - line.dist = node.text.line.dist, - main.y = clone.out$height, - panel.height = clone.out$height, - panel.width = clone.out$width, - xlims = clone.out$xlims, - ymax = clone.out$ymax, - cex = node.text.cex, - v = clone.out$v, - axis.type = yaxis.position, - node.radius = node.radius, - scale = scale1, - clone.out = clone.out, - alternating = FALSE - ); - - clone.out$grobs <- c(clone.out$grobs, list(node.text.grobs)); - } - - if (!is.null(main)) { - add.main(clone.out, main, main.cex, main.y, size.units); - } - - return(clone.out); + if (add.normal == TRUE) { + add.normal(clone.out,node.radius,label.cex, normal.cex) + } + + if (yaxis.position != 'none' & scale.bar == FALSE) { + add.axes( + clone.out, + yaxis.position, + scale1 = scale1, + scale2 = scale2, + yat = yat, + axis.label.cex = axis.label.cex, + axis.cex = axis.cex, + no.ccf = no.ccf, + xaxis.label = xaxis.label, + yaxis1.label = yaxis1.label, + yaxis2.label = yaxis2.label + ); + } + + if (scale.bar) { + scale.lengths <- prep.scale.length( + tree, + scale.size.1, + scale.size.2 + ); + + add.scale.bar( + clone.out, + scale1, + scale2, + yaxis1.label = yaxis1.label, + yaxis2.label = yaxis2.label, + scale.length = scale.lengths, + main.cex = axis.label.cex$y, + label.cex = axis.cex$y, + pos = scale.bar.coords, + padding = scale.padding + ); + } + + if (add.node.text == TRUE & !is.null(text.df)) { + node.text.grobs <- add.text2( + clone.out$tree, + text.df, + label.nodes = text.on.nodes, + line.dist = node.text.line.dist, + main.y = clone.out$height, + panel.height = clone.out$height, + panel.width = clone.out$width, + xlims = clone.out$xlims, + ymax = clone.out$ymax, + cex = node.text.cex, + v = clone.out$v, + axis.type = yaxis.position, + node.radius = node.radius, + scale = scale1, + clone.out = clone.out, + alternating = FALSE + ); + + clone.out$grobs <- c(clone.out$grobs, list(node.text.grobs)); + } + + if (!is.null(main)) { + add.main(clone.out, main, main.cex, main.y, size.units); + } + + return(clone.out); } diff --git a/R/scale.bar.R b/R/scale.bar.R new file mode 100644 index 0000000..b2bba1b --- /dev/null +++ b/R/scale.bar.R @@ -0,0 +1,252 @@ +################################################################################################### +# prep.scale.length +# +# Description: +# - Prepares the scale lengths for the tree plot based on the provided tree object and scale sizes. +# +# Arguments: +# - tree Tree data frame containing 'length1' and optionally 'length2' elements. +# - scale.size.1 The desireds size for the first scale bar. +# - scale.size.2 The desireds size for the second scale bar (optional). +# +# Returns: +# - A vector of length 2 containing the prepared scale lengths. +# - If 'scale.size.1' or 'scale.size.2' is NA, the corresponding tree length is used. + +prep.scale.length <- function( + tree, + scale.size.1, + scale.size.2 + ) { + + scale.lengths <- c(scale.size.1, scale.size.2); + tree.lengths <- c( + auto.scale.length(tree$length1), + if ('length2' %in% names(tree)) auto.scale.length(tree$length2) else NA + ); + + # if scale.length is NA, replace with tree.lengths + scale.lengths[is.na(scale.lengths)] <- tree.lengths[is.na(scale.lengths)]; + + return(scale.lengths); + } + +################################################################################################### +# auto.scale.length +# +# Description: +# - Automatically determines an appropriate scale length based on the provided edge lengths. +# +# Arguments: +# - edge.lengths A vector of edge lengths. +# +# Returns: +# - The adjusted scale length, which is the median of the non-zero edge lengths rounded down to the nearest power of 10. + +auto.scale.length <- function(edge.lengths) { + scale.length <- median(edge.lengths[edge.lengths > 0], na.rm = TRUE); + adjusted.length <- 10 ** floor(log10(as.numeric(scale.length))); + return(adjusted.length); + } + +################################################################################################### +# add.scale.bar +# +# Description: +# - Adds a scale bar to the 'clone.out' object. +# - Can add one or two scale bars based on the provided arguments. +# - Utilizes the 'create.scale.bar' and 'most.common.value' helper functions. +# +# Arguments: +# - clone.out The plot object to which the scale bar(s) will be added. +# - scale.length A vector of length 2 specifying the scale lengths for the two scale bars. +# - scale1 The scale value for the first scale bar. +# - scale2 The scale value for the second scale bar. +# - yaxis1.label The label for the first scale bar. +# - yaxis2.label The label for the second scale bar (optional). +# - pos A vector of length 2 specifying the position (x, y) of the scale bar(s). +# - padding The padding between the two scale bars (if both are present). +# - ... Additional arguments passed to 'create.scale.bar'. +# +# Returns: +# - The modified 'clone.out' object with the added scale bar(s). + +add.scale.bar <- function( + clone.out, + scale.length, + scale1, + scale2, + yaxis1.label, + yaxis2.label, + pos, + padding, + ... + ) { + + # Necessary to get the right positioning + vp.unclipped <- make.plot.viewport(clone.out, clip = 'off'); + + # Generate the first scale bar + scale.bar1 <- create.scale.bar( + main = yaxis1.label, + scale.length = list( + label = scale.length[1], + length = scale.length[1] + ), + edge.col = most.common.value(clone.out$v$edge.colour.1), + edge.width = most.common.value(clone.out$v$edge.width.1), + edge.type = most.common.value(clone.out$v$edge.type.1), + left.x = pos[1], + top.y = pos[2], + ... + ); + + clone.out$grobs <- c( + clone.out$grobs, + list(gTree(children = gList(scale.bar1), vp = vp.unclipped)) + ); + + # Create second scalebar if specified + if (!is.null(yaxis2.label)) { + scale.bar2 <- create.scale.bar( + main = yaxis2.label, + scale.length = list( + label = scale.length[2], + length = scale.length[2] / scale1 * scale2 + ), + edge.col = most.common.value(clone.out$v$edge.colour.2), + edge.width = most.common.value(clone.out$v$edge.width.2), + edge.type = most.common.value(clone.out$v$edge.type.2), + left.x = pos[1], + top.y = pos[2] + (padding / 10), + ... + ); + clone.out$grobs <- c( + clone.out$grobs, + list(gTree(children = gList(scale.bar2), vp = vp.unclipped)) + ); + } + } + +################################################################################################### +# most.common.value +# +# Description: +# - Finds the most common value in a vector. +# +# Arguments: +# - x The input vector. +# +# Returns: +# - The most common value in the input vector, or NULL if the input is NULL. + +most.common.value <- function(x) { + if (is.null(x)) { + return(NULL); + } + n.table <- table(x); + return(names(n.table)[which.max(n.table)]); + } + +################################################################################################### +# create.scale.bar +# +# Description: +# - Creates a scale bar grob (graphical object) with a title, scale line, ticks, and labels. +# +# Arguments: +# - main The title of the scale bar. +# - scale.length A list containing the length of the scale bar and its label. +# - left.x The left x-coordinate of the scale bar viewport in normalized parent coordinates (npc). +# - top.y The top y-coordinate of the scale bar viewport in normalized parent coordinates (npc). +# - edge.col The color of the scale bar line and ticks. +# - edge.width The width of the scale bar line and ticks in points. +# - edge.type The line type of the scale bar line (e.g., 'solid', 'dashed', 'dotted'). +# - main.cex The character expansion factor for the scale bar title. +# - label.cex The character expansion factor for the scale bar labels. +# +# Returns: +# - A gTree object representing the scale bar and label. +# +# Details: +# - The function creates a viewport (scale.vp) to place the scale bar within a parent vp without scaling distortion. +# - It defines the coordinates and sizes of the scale bar elements (title, scale line, ticks, and labels) + +create.scale.bar <- function( + main, + scale.length, + left.x, + top.y, + edge.col, + edge.width, + edge.type, + main.cex, + label.cex + ) { + + # Viewport for the scale bar that centers it without scaling distortion + scale.vp <- viewport( + x = unit(left.x, 'npc'), # Centered in the parent viewport + y = unit(top.y, 'npc'), # Adjust y-position based on parent viewport + width = unit(1, 'native'), # Native units for scale length + height = unit(1, 'native'), # Matches native scaling + just = 'center' + ); + + edge.width <- unit(edge.width, 'points'); + main.size <- unit(main.cex * 12, 'points'); + label.size <- unit(label.cex * 12, 'points'); + + # Define coordinates within scale.vp + vp.x <- unit(0.5, 'npc'); + vp.y <- unit(0.5, 'npc'); + xat <- vp.x + unit(c(-1, 1) * (scale.length$length / 2), 'native'); + + title <- textGrob( + label = main, + x = vp.x, + y = vp.y + main.size, + gp = gpar( + cex = main.cex + ) + ); + + scale.line <- segmentsGrob( + x0 = xat[1], + x1 = xat[2], + y0 = vp.y, + y1 = vp.y, + gp = gpar( + col = edge.col, + lwd = edge.width, + lty = edge.type, + lineend = 'butt' + ) + ); + + tick.length <- edge.width + (label.size / 4); + ticks <- segmentsGrob( + x0 = xat, + x1 = xat, + y0 = vp.y + (edge.width / 2.5), + y1 = vp.y - tick.length, + default.units = 'native', + gp = gpar( + lineend = 'butt' + ) + ); + + tick.labels <- textGrob( + label = c(0, scale.length$label), + x = xat, + y = vp.y - tick.length * 2.5, + gp = gpar( + cex = label.cex + ) + ); + + return(gTree( + children = gList(title, scale.line, ticks, tick.labels), + vp = scale.vp + )); + } diff --git a/man/SRCGrob.Rd b/man/SRCGrob.Rd index 70a2fea..85ac961 100644 --- a/man/SRCGrob.Rd +++ b/man/SRCGrob.Rd @@ -20,13 +20,13 @@ SRCGrob( ylab.cex = 1.55, xaxis.cex = 1.45, yaxis.cex = 1.45, - xaxis.label = "CP", + xaxis.label = 'CP', label.cex = NA, node.text.cex = 0.85, main.y = NULL, main.cex = 1.7, node.text.line.dist = 0.1, - colour.scheme = "grey", + colour.scheme = 'grey', add.normal = FALSE, use.radians = FALSE, normal.cex = 1, @@ -35,58 +35,69 @@ SRCGrob( polygon.shape = 3, polygon.width = 1.2, length.from.node.edge = TRUE, - size.units = "npc" + size.units = 'npc', + scale.bar = FALSE, + scale.bar.coords = c(0.5, 1), + scale.size.1 = NA, + scale.size.2 = NA, + scale.padding = 1 + ); } \arguments{ - \item{tree}{Tree structure data.frame} - \item{node.text}{Dataframe for text labels to be displayed next to nodes} - \item{main}{Main plot title} - \item{horizontal.padding}{ - Increase/reduce the plot's horizontal padding proportionally. - A positive value will expand the padding, and a negative value will reduce it. - } - \item{scale1}{ - Proportionally scale the values of the first branch length column in the tree input. - } - \item{scale2}{ - Proportionally scale the values of the second branch length column in the tree input. - } - \item{yat}{ - Specific values to be used for the y-axis ticks. A list is required, with each element - corresponding to an axis. - } - \item{yaxis1.label}{Text label for the first, leftmost y-axis} - \item{yaxis2.label}{Text label for the second, rightmost y-axis} - \item{xlab.cex}{Font size for the x-axis label} - \item{ylab.cex}{Font size for the y-axis labels} - \item{xaxis.cex}{Font size for the x-axis tick labels} - \item{yaxis.cex}{Font size for the y-axis tick labels} - \item{xaxis.label}{Text label for the x-axis} - \item{label.cex}{Font size for the node labels} - \item{node.text.cex}{Font size for the node text} - \item{main.y}{Move the main plot title position up or down} - \item{main.cex}{Font size for the main plot title} - \item{node.text.line.dist}{ - Distance between node text and tree branches (as a value between 0 and 1) - } - \item{colour.scheme}{Vector of colour values to be used for CP polygons} - \item{add.normal}{Adds a normal} - \item{use.radians}{Unit to be used for "angle" column (degrees or radians)} - \item{normal.cex}{Font size within the normal "box"} - \item{label.nodes}{Enable/disable node labels} - \item{disable.polygons}{Disables CP polygon drawing (even when CP values are provided)} - \item{polygon.shape}{ - Changes the shape of the CP shading. - Lower values are smoother. - } -\item{polygon.width}{Width of the CP shading} - \item{length.from.node.edge}{ - Sets the branch length to be calculated from the edge of the node instead of the centre - } - \item{size.units}{Grid units to be used for all specific size/length parameters} + \item{tree}{Tree structure data.frame} + \item{node.text}{Dataframe for text labels to be displayed next to nodes} + \item{main}{Main plot title} + \item{horizontal.padding}{ + Increase/reduce the plot's horizontal padding proportionally. + A positive value will expand the padding, and a negative value will reduce it. + } + \item{scale1}{ + Proportionally scale the values of the first branch length column in the tree input. + } + \item{scale2}{ + Proportionally scale the values of the second branch length column in the tree input. + } + \item{yat}{ + Specific values to be used for the y-axis ticks. A list is required, with each element + corresponding to an axis. + } + \item{yaxis1.label}{Text label for the first, leftmost y-axis} + \item{yaxis2.label}{Text label for the second, rightmost y-axis} + \item{xlab.cex}{Font size for the x-axis label} + \item{ylab.cex}{Font size for the y-axis labels} + \item{xaxis.cex}{Font size for the x-axis tick labels} + \item{yaxis.cex}{Font size for the y-axis tick labels} + \item{xaxis.label}{Text label for the x-axis} + \item{label.cex}{Font size for the node labels} + \item{node.text.cex}{Font size for the node text} + \item{main.y}{Move the main plot title position up or down} + \item{main.cex}{Font size for the main plot title} + \item{node.text.line.dist}{ + Distance between node text and tree branches (as a value between 0 and 1) + } + \item{colour.scheme}{Vector of colour values to be used for CP polygons} + \item{add.normal}{Adds a normal} + \item{use.radians}{Unit to be used for 'angle' column (degrees or radians)} + \item{normal.cex}{Font size within the normal 'box'} + \item{label.nodes}{Enable/disable node labels} + \item{disable.polygons}{Disables CP polygon drawing (even when CP values are provided)} + \item{polygon.shape}{ + Changes the shape of the CP shading. + Lower values are smoother. + } + \item{polygon.width}{Width of the CP shading} + \item{length.from.node.edge}{ + Sets the branch length to be calculated from the edge of the node instead of the centre + } + \item{size.units}{Grid units to be used for all specific size/length parameters} + \item{scale.bar}{Set to \code{TRUE} to add a scale bar instead of y-axis} + \item{scale.bar.coords}{Coordinates for the scale bar placement \code{c(x.pos, y.pos)}} + \item{scale.size.1}{Overide size of the first scale bar} + \item{scale.size.2}{Overide size of the second scale bar} + \item{scale.padding}{Padding between scale bars if more than 1 is present} } -\value{A `grob` of class "SRCGrob"} +\value{A `grob` of class 'SRCGrob'} \author{Dan Knight} \examples{ # Simple Tree Plot @@ -166,6 +177,14 @@ SRCGrob( ); +# Scale Bar +SRCGrob( + branch.lengths.tree, + yaxis1.label = 'SNVs', + scale.bar = TRUE + ); + + # Normal SRCGrob( simple.tree,