diff --git a/NEWS b/NEWS index 196c72e..c752b04 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,12 @@ -CancerEvolutionVisualization 2.0.0 2023-11-09 +CancerEvolutionVisualization 2.0.0 2023-11-09 (Helena Winata, Dan Knight) ADDED +* Option to specify edge colour with "edge.col.1" and "edge.col.2" + columns in tree input dataframe +* Option to specify edge width using "edge.width.1" and "edge.width.2" + columns in tree input dataframe +* Option to specify edge linetype with "edge.type.1" and "edge.type.2" + columns in tree input dataframe * Support for specifying tree angles in either radians or degrees using an optional "angle" column * Generic functions to generate accompanying heatmaps @@ -14,8 +20,9 @@ UPDATE * Fixed lopsided radial tree bug REMOVED -* "node.col" parameter to SRCGrob. (Node colour only customizable through - tree input data.frame.) +* "seg1.col" and "seg2.col" parameters (replaced by tree input columns). +* "node.col" parameter to SRCGrob. (Node colour only customizable through tree input data.frame.) + -------------------------------------------------------------------------- CancerEvolutionVisualization 1.0.1 2022-10-03 (Dan Knight) diff --git a/R/SRCGrob.R b/R/SRCGrob.R index 9537900..7e96e86 100644 --- a/R/SRCGrob.R +++ b/R/SRCGrob.R @@ -18,9 +18,6 @@ SRCGrob <- function( main.y = NULL, main.cex = 1.7, node.radius = 0.1, - seg1.col = 'black', - seg2.col = 'green', - line.lwd = 3, node.text.line.dist = 0.1, colour.scheme = CancerEvolutionVisualization::colours, draw.nodes = TRUE, @@ -85,10 +82,10 @@ SRCGrob <- function( scale2 = scale2, yat = yat, wid = wid, - line.lwd = line.lwd, length.from.node.edge = length.from.node.edge, seg1.col = seg1.col, seg2.col = seg2.col, + default.branch.width = 4, add.polygons = add.polygons, sig.shape = sig.shape, spread = spread, diff --git a/R/add.segs.R b/R/add.segs.R index ef283d9..a4fc155 100644 --- a/R/add.segs.R +++ b/R/add.segs.R @@ -219,8 +219,9 @@ add.tree.segs <- function( y1 = tree.segs1$tipy, default.units = 'native', gp = gpar( - col = seg1.col, - lwd = line.lwd + col = clone.out$v$edge.colour.1, + lwd = clone.out$v$edge.width.1, + lty = clone.out$v$edge.type.1 ) ); @@ -236,8 +237,9 @@ add.tree.segs <- function( y1 = tree.segs2$tipy, default.units = 'native', gp = gpar( - col = seg2.col, - lwd = line.lwd + col = clone.out$v$edge.colour.2, + lwd = clone.out$v$edge.width.2, + lty = clone.out$v$edge.type.2 ) ); } diff --git a/R/make.clone.tree.grobs.R b/R/make.clone.tree.grobs.R index d2b9157..704112e 100644 --- a/R/make.clone.tree.grobs.R +++ b/R/make.clone.tree.grobs.R @@ -6,7 +6,7 @@ make.clone.tree.grobs <- function( scale2, wid, yat, - line.lwd, + default.branch.width, length.from.node.edge, seg1.col, seg2.col, @@ -121,7 +121,7 @@ make.clone.tree.grobs <- function( get.CP.polygons(clone.out); } - add.tree.segs(clone.out, node.radius, line.lwd, scale1, seg1.col, seg2.col); + add.tree.segs(clone.out, node.radius, default.branch.width, scale1, seg1.col, seg2.col); if (!is.null(cluster.list)) { message(paste( diff --git a/R/prep.branch.lengths.R b/R/prep.branch.lengths.R index ca8f2df..f26870b 100644 --- a/R/prep.branch.lengths.R +++ b/R/prep.branch.lengths.R @@ -11,9 +11,9 @@ get.default.branch.length.colnames <- function(num.columns) { } } -get.default.branch.lengths <- function(num.rows) { - lengths <- data.frame(a = rep(1, times = num.rows)); - colnames(lengths) <- get.default.branch.length.colnames(1); +get.default.branch.lengths <- function(num.rows, num.cols) { + lengths <- data.frame(a = rep(num.cols, times = num.rows)); + colnames(lengths) <- get.default.branch.length.colnames(num.cols); return(lengths); } @@ -78,12 +78,16 @@ prep.branch.lengths <- function(tree.df) { ) ); + # TODO: Automatically create length2 if an edge.style.2 column is present. if (length(length.cols) > 0) { lengths.df <- data.frame(tree.df[, length.cols]); colnames(lengths.df) <- get.default.branch.length.colnames(length(length.cols)); return(lengths.df); } else { - return(get.default.branch.lengths(nrow(tree.df))); + return(get.default.branch.lengths( + num.rows = nrow(tree.df), + num.cols = 1 + )); } } diff --git a/R/prep.tree.R b/R/prep.tree.R index ed3fb70..6cd7bbc 100644 --- a/R/prep.tree.R +++ b/R/prep.tree.R @@ -54,6 +54,34 @@ prep.tree <- function( } } + tree.df <- prep.edge.colours(tree.df); + + default.edge.type <- 'solid'; + if ('edge.type.1' %in% colnames(tree.df)) { + tree.df$edge.type.1[is.na(tree.df$edge.type.1)] <- default.edge.type; + } else { + tree.df$edge.type.1 <- default.edge.type; + } + + if ('edge.type.2' %in% colnames(tree.df)) { + tree.df$edge.type.2[is.na(tree.df$edge.type.2)] <- default.edge.type; + } else { + tree.df$edge.type.2 <- default.edge.type; + } + + default.edge.width <- 3; + if ('edge.width.1' %in% colnames(tree.df)) { + tree.df$edge.width.1[is.na(tree.df$edge.width.1)] <- default.edge.width; + } else { + tree.df$edge.width.1 <- default.edge.width; + } + + if ('edge.width.2' %in% colnames(tree.df)) { + tree.df$edge.width.2[is.na(tree.df$edge.width.2)] <- default.edge.width; + } else { + tree.df$edge.width.2 <- default.edge.width; + } + tree.df <- reorder.nodes(tree.df); # Include -1 value for root node. @@ -133,6 +161,12 @@ prep.tree <- function( border.width = c(NA, tree.df$border.width), parent = as.numeric(c(NA,tree.df$parent)), excluded = c(TRUE, rep(FALSE, nrow(tree.df))), + edge.colour.1 = c(NA, tree.df$edge.col.1), + edge.colour.2 = c(NA, tree.df$edge.col.2), + edge.type.1 = c(NA, tree.df$edge.type.1), + edge.type.2 = c(NA, tree.df$edge.type.2), + edge.width.1 = c(NA, tree.df$edge.width.1), + edge.width.2 = c(NA, tree.df$edge.width.2), bell = c(FALSE, rep(bells, nrow(tree.df))), alpha = rep(0.5, (nrow(tree.df) + 1)), stringsAsFactors = FALSE @@ -257,6 +291,39 @@ get.y.axis.position <- function(tree.colnames) { return(y.axis.position); } +prep.edge.colours <- function(tree.df) { + edge.colours <- list(); + + default.edge.colours <- c('black', 'green'); + edge.colour.column.names <- sapply( + 1:2, + function(i) paste('edge', 'col', i, sep = '.') + ); + + for (i in 1:length(edge.colour.column.names)) { + column.name <- edge.colour.column.names[i]; + default.colour <- default.edge.colours[i]; + + if (column.name %in% colnames(tree.df)) { + tree.df[is.na(tree.df[, column.name]), column.name] <- default.colour; + } else { + tree.df[, column.name] <- default.colour; + } + } + + return(tree.df); + } + +prep.edge.colour.column <- function(tree.df, column.name, default.value) { + if (column.name %in% colnames(tree.df)) { + values <- tree.df[, column.name]; + values[is.na(values)] <- default.value; + return(values); + } else { + return(rep(default.value, nrow(tree.df))); + } + } + prep.node.label.colours <- function(tree.df) { node.col.error.message <- 'Cannot prepare node label colour without node colour values.'; diff --git a/man/SRCGrob.Rd b/man/SRCGrob.Rd index 7c9b208..a851492 100644 --- a/man/SRCGrob.Rd +++ b/man/SRCGrob.Rd @@ -26,9 +26,6 @@ SRCGrob( main.y = NULL, main.cex = 1.7, node.radius = 0.1, - seg1.col = "black", - seg2.col = "green", - line.lwd = 3, node.text.line.dist = 0.1, colour.scheme = CancerEvolutionVisualization::colours, draw.nodes = TRUE, @@ -72,9 +69,6 @@ SRCGrob( \item{main.y}{Move the main plot title position up or down} \item{main.cex}{Font size for the main plot title} \item{node.radius}{Node size} - \item{seg1.col}{Colour of the first set of tree branch segments} - \item{seg2.col}{Colour of the second set of tree branch segments} - \item{line.lwd}{Branch segment thickness} \item{node.text.line.dist}{ Distance between node text and tree branches (as a value between 0 and 1) } diff --git a/tests/testthat/data/branching.fixed.data.Rda b/tests/testthat/data/branching.fixed.data.Rda index 53ec65e..b1c6ce7 100644 Binary files a/tests/testthat/data/branching.fixed.data.Rda and b/tests/testthat/data/branching.fixed.data.Rda differ diff --git a/tests/testthat/data/branching.fixed.plots.Rda b/tests/testthat/data/branching.fixed.plots.Rda index 9b1affe..6b8442b 100644 Binary files a/tests/testthat/data/branching.fixed.plots.Rda and b/tests/testthat/data/branching.fixed.plots.Rda differ diff --git a/tests/testthat/data/branching.radial.data.Rda b/tests/testthat/data/branching.radial.data.Rda index db8951b..31df4aa 100644 Binary files a/tests/testthat/data/branching.radial.data.Rda and b/tests/testthat/data/branching.radial.data.Rda differ diff --git a/tests/testthat/data/branching.radial.plots.Rda b/tests/testthat/data/branching.radial.plots.Rda index a7e3386..8db013c 100644 Binary files a/tests/testthat/data/branching.radial.plots.Rda and b/tests/testthat/data/branching.radial.plots.Rda differ diff --git a/tests/testthat/data/linear.data.Rda b/tests/testthat/data/linear.data.Rda index 9ced492..ef03754 100644 Binary files a/tests/testthat/data/linear.data.Rda and b/tests/testthat/data/linear.data.Rda differ diff --git a/tests/testthat/data/linear.plots.Rda b/tests/testthat/data/linear.plots.Rda index 44bc67a..506b831 100644 Binary files a/tests/testthat/data/linear.plots.Rda and b/tests/testthat/data/linear.plots.Rda differ diff --git a/tests/testthat/helper-multitest.R b/tests/testthat/helper-multitest.R index 0d2a35e..d83c5df 100644 --- a/tests/testthat/helper-multitest.R +++ b/tests/testthat/helper-multitest.R @@ -5,9 +5,6 @@ create.test.tree <- function(tree, node.text, sample, ...) { node.radius = 0.1, node.text.cex = 0.85, scale1 = 0.9, - seg1.col = 'navy', - seg2.col = 'gold', - line.lwd = 4, yaxis1.label = 'PGA', yaxis2.label = 'SNV', xaxis.label = 'CP', diff --git a/tests/testthat/test-prep.branch.lengths.R b/tests/testthat/test-prep.branch.lengths.R index 0fab239..0d0f391 100644 --- a/tests/testthat/test-prep.branch.lengths.R +++ b/tests/testthat/test-prep.branch.lengths.R @@ -69,7 +69,10 @@ test_that( expected.length <- 10; expect_equal( - nrow(get.default.branch.lengths(expected.length)), + nrow(get.default.branch.lengths( + num.rows = expected.length, + num.cols = 1 + )), expected.length ); }); @@ -80,7 +83,7 @@ test_that( expect_true(all( apply( - get.default.branch.lengths(3), + get.default.branch.lengths(num.rows = 3, num.cols = 1), MARGIN = 1, FUN = function(x) { x == expected.value; diff --git a/tests/testthat/test-prep.tree.R b/tests/testthat/test-prep.tree.R index 559006a..c4c1aa8 100644 --- a/tests/testthat/test-prep.tree.R +++ b/tests/testthat/test-prep.tree.R @@ -47,6 +47,123 @@ test_that( ); }); +test_that( + 'prep.tree passes valid edge 1 colour values', { + tree.df <- data.frame( + parent = c(NA, 1:3), + edge.col.1 = 'red' + ); + + result <- prep.tree( + tree.df, + text.df = NULL, + colour.scheme = colours + ); + + result.edge.colours <- result$in.tree.df$edge.colour.1; + expected.edge.colours <- c(NA, tree.df$edge.col.1); + + expect_equal(result.edge.colours, expected.edge.colours); + }); + + +test_that( + 'prep.tree passes valid edge 2 colour values', { + tree.df <- data.frame( + parent = c(NA, 1:3), + edge.col.2 = 'red' + ); + + result <- prep.tree( + tree.df, + text.df = NULL, + colour.scheme = colours + ); + + result.edge.colours <- result$in.tree.df$edge.colour.2; + expected.edge.colours <- c(NA, tree.df$edge.col.2); + + expect_equal(result.edge.colours, expected.edge.colours); + }); + +test_that( + 'prep.tree passes valid edge 1 width values', { + tree.df <- data.frame( + parent = c(NA, 1:3), + edge.width.1 = 1:4 + ); + + result <- prep.tree( + tree.df, + text.df = NULL, + colour.scheme = colours + ); + + result.edge.widths <- result$in.tree.df$edge.width.1; + expected.edge.widths <- c(NA, tree.df$edge.width.1); + + expect_equal(result.edge.widths, expected.edge.widths); + }); + + +test_that( + 'prep.tree passes valid edge 2 width values', { + tree.df <- data.frame( + parent = c(NA, 1:3), + edge.width.2 = 1:4 + ); + + result <- prep.tree( + tree.df, + text.df = NULL, + colour.scheme = colours + ); + + result.edge.widths <- result$in.tree.df$edge.width.2; + expected.edge.widths <- c(NA, tree.df$edge.width.2); + + expect_equal(result.edge.widths, expected.edge.widths); + }); + +test_that( + 'prep.tree passes valid edge 1 linetype values', { + tree.df <- data.frame( + parent = c(NA, 1:3), + edge.type.1 = 'dotted' + ); + + result <- prep.tree( + tree.df, + text.df = NULL, + colour.scheme = colours + ); + + result.edge.linetypes <- result$in.tree.df$edge.type.1; + expected.edge.linetypes <- c(NA, tree.df$edge.type.1); + + expect_equal(result.edge.linetypes, expected.edge.linetypes); + }); + + +test_that( + 'prep.tree passes valid edge 2 linetype values', { + tree.df <- data.frame( + parent = c(NA, 1:3), + edge.type.2 = 'solid' + ); + + result <- prep.tree( + tree.df, + text.df = NULL, + colour.scheme = colours + ); + + result.edge.linetypes <- result$in.tree.df$edge.type.2; + expected.edge.linetypes <- c(NA, tree.df$edge.type.2); + + expect_equal(result.edge.linetypes, expected.edge.linetypes); + }); + test_that( 'prep.tree.parent handles values of 0', { parent <- c(0:5); @@ -332,6 +449,106 @@ test_that( expect_equal(yaxis.position, expected.position); }); +test_that( + 'prep.edge.colours handles valid colour columns', { + tree <- data.frame( + parent = 1:10, + edge.col.1 = c('red'), + edge.col.2 = c('blue') + ); + + result <- prep.edge.colours(tree); + + expect_equal(result, tree); + }); + +test_that( + 'prep.edge.colours adds missing colour columns', { + tree <- data.frame( + parent = 1:10 + ); + + result <- prep.edge.colours(tree); + expected.edge.colnames <- sapply( + 1:2, function(i) paste('edge', 'col', i, sep = '.') + ); + edge.columns.found <- expected.edge.colnames %in% colnames(result); + + expect_true(all(edge.columns.found)); + }); + +test_that( + 'prep.edge.colours includes all tree columns', { + tree <- data.frame( + parent = c(NA, 1:9), + length1 = 1:10 + ); + + result <- prep.edge.colours(tree); + tree.columns.found <- colnames(tree) %in% colnames(result); + + expect_true(all(tree.columns.found)); + }); + +test_that( + 'prep.edge.colour.column handles valid column values', { + tree <- data.frame( + parent = c(NA, 1:9) + ); + + valid.colour <- 'green'; + column.name <- 'edge.col.1'; + tree[, column.name] <- valid.colour; + + result <- prep.edge.colour.column(tree, column.name, 'blue'); + + expect_equal(result, tree[, column.name]); + }); + +test_that( + 'prep.edge.colour.column replaces NA with default', { + tree <- data.frame( + parent = c(NA, 1:9) + ); + + column.name <- 'edge.col.1'; + tree[, column.name] <- 'green'; + + NA.indices <- 2:4; + tree[NA.indices, column.name] <- NA; + + default.colour <- 'black'; + result <- prep.edge.colour.column(tree, column.name, default.colour); + result.replaced <- result[NA.indices] == default.colour; + + expect_true(all(result.replaced)); + }); + +test_that( + 'prep.edge.colour.column returns correct length with missing column', { + tree <- data.frame( + parent = c(NA, 1:9) + ); + + result <- prep.edge.colour.column(tree, 'edge.col.1', 'black'); + expected.num.rows <- nrow(tree); + + expect_equal(length(result), expected.num.rows); + }); + +test_that( + 'prep.edge.colour.column returns default with missing column', { + tree <- data.frame( + parent = c(NA, 1:9) + ); + + default.colour <- 'black'; + result <- prep.edge.colour.column(tree, 'edge.col.1', default.colour); + match.default <- result == default.colour; + + expect_true(all(match.default)); + }); + test_that( 'prep.node.label.colours returns valid values', { node.label.colours <- c('green', 'white'); diff --git a/vignettes/UserGuide.Rmd b/vignettes/UserGuide.Rmd index 0007efa..cb86a75 100644 --- a/vignettes/UserGuide.Rmd +++ b/vignettes/UserGuide.Rmd @@ -96,7 +96,7 @@ grid.draw(branch.lengths.tree); ### Ex. 3 Styling the Tree -CEV gives the user control over numerous visual aspects of the tree. By specifying optional columns and values in the tree input data.frame, the user has individual control of edge colour, width, and line type. +CEV gives the user control over numerous visual aspects of the tree. By specifying optional columns and values in the tree input data.frame, the user has individual control of the colour, width, and line type of each node, label border, and edge. #### Optional Style Columns | Style | Column | @@ -106,6 +106,10 @@ CEV gives the user control over numerous visual aspects of the tree. By specifyi | Node Border Colour | `border.col` | | Node Border Width | `border.width` | | Node Border Line Type | `border.type` | +| | | +| Edge Colour | `edge.col.1`, `edge.col.2` | +| Edge Width | `edge.width.1`, `edge.width.2` | +| Edge Line Type | `edge.type.1`, `edge.type.2` | Default values replace missing columns and `NA` values, allowing node-by-node, and edge-by-edge control as needed. For sparsely defined values (for example, only specifying a single edge), it can be convenient to initialize a column with `NA`s, then manually assign specific nodes as needed. @@ -123,17 +127,19 @@ Valid values for line type columns are based on lattice's values (with some addi | `'longdash'` | | `'twodash'` | -#### Styled Nodes +#### Styled Tree ```{r echo=F} node.style <- tree.input[, c( - 'parent', + 'parent', 'length1', 'length2', 'node.col', 'node.label.col', - 'border.col', 'border.width', 'border.type' + 'border.col', 'border.width', 'border.type', + 'edge.col.1', 'edge.type.1', + 'edge.col.2', 'edge.width.2' )]; knitr::kable( - node.style, + node.style[, !(colnames(node.style) %in% c('parent', 'length1', 'length2'))], row.names = TRUE ); ``` diff --git a/vignettes/data/input.examples.Rda b/vignettes/data/input.examples.Rda index a2546c1..903f6e4 100644 Binary files a/vignettes/data/input.examples.Rda and b/vignettes/data/input.examples.Rda differ