diff --git a/NAMESPACE b/NAMESPACE index ec8164f..ad65708 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +S3method(plot,giottoTree) S3method(print,giotto_plot_save_param) export("sankeyLabel<-") export(FSV_show) diff --git a/NEWS.md b/NEWS.md index 17a8a0a..f9edcf1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ # GiottoVisuals 0.2.16 (in development) ## enhancements +- `plot()` on a `giottoTree` from `Giotto::calculateClusterTree()` gains `what = c("tree", "heatmap")`. `"tree"` is the plain `hclust` plot, as before; `"heatmap"` draws the correlation matrix the tree was built from, in leaf order with its branches on both axes, so the colours and branches always agree. - `showClusterHeatmap()` gains `cluster_custom_order`, to lay the clusters out in a given order, such as a cluster tree's leaf order (`tree$labels[tree$order]`). The clusters are then not clustered and no dendrogram is drawn, so the plot shows the correlations in that arrangement without claiming a structure. `expression_values` also accepts any expression name, not only `"normalized"` / `"scaled"` / `"custom"`. - image layers in spatial plots sample with `terra::spatSample(method = "display")`, which reads from the image's overviews when it has them, instead of `"regular"`, which ignored them. On a 10x JPEG-2000 `.ome.tif`, adding a whole-image layer went from ~2.3 s to ~0.2 s. Plotted pixel values now come from the overview level, so they are averaged rather than exact. - plot functions accept `view` and `space`: the name of a view or coordinate-frame recipe slotted on the object. The object is pre-narrowed through `GiottoClass::resolveRecipe()` once, so every slot read in a plot shares one resolver pass. The pass is scoped to the plot's own `spat_unit` / `feat_type` (GiottoClass >= 0.7.3), so on an object whose units do not share cell IDs, such as cells and nuclei, `spatPlot2D(view = )` no longer fails with "invalid extent" from a unit the view had narrowed to nothing. Added to `spatPlot2D()`, `spatInSituPlotPoints()`, `spatInSituPlotHex()`, `spatInSituPlotDensity()`, `spatDeconvPlot()`, `spatFeatPlot2D()` and `spatCellPlot2D()`. Plots that read no coordinates take `view` only, since a space changes nothing they draw: the heatmap / violin / ridge / dot / dendrogram family and `dimPlot2D()`, `dimFeatPlot2D()`, `dimCellPlot2D()`. diff --git a/R/plot_cluster_tree.R b/R/plot_cluster_tree.R index a925d5d..1b1d167 100644 --- a/R/plot_cluster_tree.R +++ b/R/plot_cluster_tree.R @@ -111,6 +111,66 @@ showClusterDendrogram <- function(gobject, } +#' @title Plot a cluster tree +#' @name plot.giottoTree +#' @description Draw a `giottoTree` from `Giotto::calculateClusterTree()`, +#' either as the tree itself or as the correlation matrix it was built from. +#' @param x a `giottoTree` +#' @param what `"tree"` (default) draws the dendrogram with the base `hclust` +#' method. `"heatmap"` draws the correlation matrix recorded on the tree, in +#' leaf order, with the tree's branches on both axes. +#' @param ... passed to the base `hclust` plot method (e.g. `hang = -1`), or +#' to `ComplexHeatmap::Heatmap()` +#' @returns invisibly: `x` for `"tree"`, the `Heatmap` object for `"heatmap"` +#' @details +#' The heatmap shows the matrix **the branches were built from**, so the two +#' always agree. It describes the tree, not whatever object is current: to see +#' correlations recomputed from an object in a tree's order, use +#' [showClusterHeatmap()] with `cluster_custom_order = +#' tree$labels[tree$order]`, which draws no branches. For the tree with +#' annotations and per-cluster evidence, see [plotClusterTree()]. +#' @examples +#' g <- GiottoData::loadGiottoMini("visium") +#' tree <- Giotto::calculateClusterTree(g, cluster_column = "leiden_clus") +#' plot(tree, hang = -1) +#' plot(tree, what = "heatmap") +#' @method plot giottoTree +#' @export +plot.giottoTree <- function(x, what = c("tree", "heatmap"), ...) { + what <- match.arg(what) + if (identical(what, "tree")) { + # the plain hclust method, without `what` reaching its graphics args + y <- x + class(y) <- setdiff(class(x), "giottoTree") + graphics::plot(y, ...) + return(invisible(x)) + } + + cm <- attr(x, "cor_matrix") + if (is.null(cm)) { + stop("[plot.giottoTree] this tree carries no correlation matrix. ", + "To draw correlations from an object in its order, use ", + "`showClusterHeatmap(cluster_custom_order = ", + "tree$labels[tree$order])`.", call. = FALSE) + } + package_check(pkg_name = "ComplexHeatmap", repository = "Bioc") + p <- attr(x, "params") + title <- sprintf("%s correlation of cluster means%s", + p$cor %null% "", if (is.null(p$n_feats)) "" else + sprintf(", %d features", p$n_feats)) + hm <- ComplexHeatmap::Heatmap( + matrix = cm[x$labels, x$labels, drop = FALSE], + cluster_rows = x, + cluster_columns = x, + name = "r", + column_title = trimws(title), + ... + ) + ComplexHeatmap::draw(hm) + invisible(hm) +} + + #' @title plotClusterTree #' @name plotClusterTree #' @description Publication figure for an annotated cluster tree: the diff --git a/man/plot.giottoTree.Rd b/man/plot.giottoTree.Rd new file mode 100644 index 0000000..62df40b --- /dev/null +++ b/man/plot.giottoTree.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot_cluster_tree.R +\name{plot.giottoTree} +\alias{plot.giottoTree} +\title{Plot a cluster tree} +\usage{ +\method{plot}{giottoTree}(x, what = c("tree", "heatmap"), ...) +} +\arguments{ +\item{x}{a \code{giottoTree}} + +\item{what}{\code{"tree"} (default) draws the dendrogram with the base \code{hclust} +method. \code{"heatmap"} draws the correlation matrix recorded on the tree, in +leaf order, with the tree's branches on both axes.} + +\item{...}{passed to the base \code{hclust} plot method (e.g. \code{hang = -1}), or +to \code{ComplexHeatmap::Heatmap()}} +} +\value{ +invisibly: \code{x} for \code{"tree"}, the \code{Heatmap} object for \code{"heatmap"} +} +\description{ +Draw a \code{giottoTree} from \code{Giotto::calculateClusterTree()}, +either as the tree itself or as the correlation matrix it was built from. +} +\details{ +The heatmap shows the matrix \strong{the branches were built from}, so the two +always agree. It describes the tree, not whatever object is current: to see +correlations recomputed from an object in a tree's order, use +\code{\link[=showClusterHeatmap]{showClusterHeatmap()}} with \code{cluster_custom_order = tree$labels[tree$order]}, which draws no branches. For the tree with +annotations and per-cluster evidence, see \code{\link[=plotClusterTree]{plotClusterTree()}}. +} +\examples{ +g <- GiottoData::loadGiottoMini("visium") +tree <- Giotto::calculateClusterTree(g, cluster_column = "leiden_clus") +plot(tree, hang = -1) +plot(tree, what = "heatmap") +} diff --git a/tests/testthat/test-plot-cluster-tree.R b/tests/testthat/test-plot-cluster-tree.R index c288c80..957d8d1 100644 --- a/tests/testthat/test-plot-cluster-tree.R +++ b/tests/testthat/test-plot-cluster-tree.R @@ -189,3 +189,29 @@ test_that("plotClusterTree counts within the tree's view, and flags another", { "the tree was built with `view = \"v\"`" ), "clusters with no leaf") }) + + +test_that("plot(tree) draws the tree, and what = 'heatmap' its own matrix", { + skip_if_not_installed("ComplexHeatmap") + fx <- .pct_gobject() + tree <- .pct_tree(fx$gobject) + grDevices::pdf(NULL) + on.exit(grDevices::dev.off(), add = TRUE) + + expect_no_warning(expect_identical(plot(tree, hang = -1), tree)) + + hm <- plot(tree, what = "heatmap") + expect_s4_class(hm, "Heatmap") + # the matrix the branches were built from, so the two cannot disagree + expect_identical(hm@matrix, + attr(tree, "cor_matrix")[tree$labels, tree$labels]) + expect_identical(hm@row_dend_param$obj$merge, tree$merge) +}) + + +test_that("a tree without its matrix says where to go instead", { + fx <- .pct_gobject() + tree <- .pct_tree(fx$gobject) + attr(tree, "cor_matrix") <- NULL + expect_error(plot(tree, what = "heatmap"), "cluster_custom_order") +})