10fbf31c |
# == title
|
ad35494a |
# The AnnotationFunction class
|
10fbf31c |
#
# == details
|
1a56796e |
# The heatmap annotation is basically graphics aligned to the heatmap columns
# if it is column annotation or heatmap rows if it is row annotation, while
# there is no restrictions for the graphic types, e.g. it can be heatmap-like
# annotation or points. Here the AnnotationFunction class is designed for
# creating complex and flexible annotation graphics. As the main part of the class, it uses
# a user-defined function to define the graphics. It also keeps information of
# the size of the plotting regions of the annotation. And most importantly, it
# allows subsetting of the annotation to draw a subset of the graphics, which
|
ad35494a |
# is the base for the splitting of the annotations.
#
# See `AnnotationFunction` constructor for details.
|
10fbf31c |
#
|
783c77fd |
AnnotationFunction = setClass("AnnotationFunction",
slots = list(
which = "character",
fun_name = "character",
width = "ANY",
height = "ANY",
|
c4a66bf9 |
n = "ANY",
|
783c77fd |
var_env = "environment",
fun = "function",
subset_rule = "list",
|
261bfec2 |
subsetable = "logical",
data_scale = "numeric",
|
933e808c |
extended = "ANY",
show_name = "logical"
|
783c77fd |
),
prototype = list(
|
261bfec2 |
fun_name = "",
|
783c77fd |
width = unit(1, "npc"),
height = unit(1, "npc"),
subset_rule = list(),
|
261bfec2 |
subsetable = FALSE,
data_scale = c(0, 1),
|
c4a66bf9 |
n = NA_integer_,
|
933e808c |
extended = unit(c(0, 0, 0, 0), "mm"),
show_name = TRUE
|
783c77fd |
)
)
|
402ff791 |
|
261bfec2 |
anno_width_and_height = function(which, width = NULL, height = NULL,
|
402ff791 |
default = unit(10, "mm")) {
|
261bfec2 |
if(which == "column") {
if(is.null(height)) {
height = default
} else {
if(!is_abs_unit(height)) {
|
c4a66bf9 |
stop_wrap("height of the annotation can only be an absolute unit.")
|
402ff791 |
} else {
height = convertHeight(height, "mm")
|
261bfec2 |
}
}
if(is.null(width)) {
width = unit(1, "npc")
}
}
if(which == "row") {
if(is.null(width)) {
width = default
} else {
if(!is_abs_unit(width)) {
|
c4a66bf9 |
stop_wrap("width of the annotation can only be an absolute unit.")
|
402ff791 |
} else {
width = convertWidth(width, "mm")
|
261bfec2 |
}
}
if(is.null(height)) {
height = unit(1, "npc")
}
}
return(list(height = height, width = width))
}
|
10fbf31c |
# == title
|
ad35494a |
# Constructor of AnnotationFunction Class
|
10fbf31c |
#
# == param
|
1a56796e |
# -fun A function which defines how to draw the annotation. See **Details** section.
|
ad35494a |
# -fun_name The name of the function. It is only used for printing the object.
|
10fbf31c |
# -which Whether it is drawn as a column annotation or a row annotation?
|
ad35494a |
# -var_import The names of the variables or the variable themselves that the annotation function depends on. See **Details** section.
# -n Number of observations in the annotation. It is not mandatory, but it is better to provide this information
# so that the higher order `HeatmapAnnotation` knows it and it can perform check on the consistency of annotations and heatmaps.
|
10fbf31c |
# -data_scale The data scale on the data axis (y-axis for column annotation and x-axis for row annotation). It is only used
|
ad35494a |
# when `decorate_annotation` is used with "native" unit coordinates.
|
10fbf31c |
# -subset_rule The rule of subsetting variables in ``var_import``. It should be set when users want the final object to
# be subsetable. See **Details** section.
# -subsetable Whether the object is subsetable?
|
1a56796e |
# -show_name It is used to turn off the drawing of annotation names in `HeatmapAnnotation`. Annotations always have names
# associated and normally they will be drawn beside the annotation graphics to tell what the annotation is about.
# e.g. the annotation names put beside the points annotation graphics. However, for some of the annotations, the names
|
b8dacfcc |
# are not necessarily to be drawn, such as text annotations drawn by `anno_text` or an empty annotation drawn by `anno_empty`.
# In this case, when ``show_names`` is set to ``FALSE``, there will be no annotation names drawn for the annotation.
|
10fbf31c |
# -width The width of the plotting region (the viewport) that the annotation is drawn. If it is a row annotation,
|
1a56796e |
# the width must be an absolute unit. Since the ``AnnotationFunction`` object is always contained by the `SingleAnnotation-class`object,
# you can only set the width of row annotations or height of column annotations, while e.g. the height of the row annotation is always ``unit(1, "npc")``
# which means it always fully filled in the parent ``SingleAnnotation`` and only in `SingleAnnotation` or even `HeatmapAnnotation`
# can adjust the height of the row annotations.
|
10fbf31c |
# -height The height of the plotting region (the viewport) that the annotation is drawn. If it is a column annotation,
# the width must be an absolute unit.
#
# == details
|
1a56796e |
# We use a normal R function defines how to draw the annotation graphics. As
# expected, the main part of the AnnotationFunction class is this function.
# The function defines how to draw at specific positions which correspond to
# rows or columns in the heatmap. The function should have three arguments:
# ``index``, ``k`` and ``n`` (the names of the arguments can be arbitory)
# where ``k`` and ``n`` are optional. ``index`` corresponds to the indices of
# rows or columns of the heatmap. The value of ``index`` is not necessarily to
# be the whole row indices or column indices of the heatmap. It can be a
# subset of the indices if the annotation is split into slices according to
# the split of the heatmap. ``index`` is always reordered according to the
# reordering of heatmap rows or columns (e.g. by clustering). So, ``index``
# actually contains a list of row or column indices for the current slice
# after row or column reordering.
|
ad35494a |
#
|
1a56796e |
# As mentioned, annotation can be split into slices. ``k`` corresponds to the
# current slice and ``n`` corresponds to the total number of slices. As you can image,
# when ``n > 1``, the annotation function will be executed for all ``k``s. The
# information of ``k`` and ``n`` sometimes can be useful, for example, we want
# to add axis ot the right side of a column annotation, if this column annotation
# is split into several slices, the axis is only drawn when``k == n``.
|
ad35494a |
#
|
1a56796e |
# Since the function only allows ``index``, ``k`` and ``n``, the function
# sometimes uses several external variables which can not be defined inside
# the function, e.g. the data points for the annotation. These variables
# should be imported into the AnnotationFunction class so that the function
# can correctly find these variables (by ``var_import`` argument).
|
ad35494a |
#
|
1a56796e |
# One important feature for AnnotationFunction class is it can be subsetable.
# To allow subsetting of the object, users need to define the rules for the
# imported variables. The rules are simple function which
# accpets the variable and indices, and returns the subset of the variable.
# The subset rule functions implemented in this package are `subset_gp`,
# `subset_matrix_by_row` and `subset_vector`. These three functions are enough
# for most of the cases.
|
ad35494a |
#
# In following, we defined three AnnotationFunction objects:
#
# 1. It needs external variable and support subsetting
#
# x = 1:10
# anno1 = AnnotationFunction(
# fun = function(index) {
# n = length(index)
# pushViewport(viewport())
# grid.points(1:n, x[index])
# popViewport()
# },
# var_imported = list(x = x),
# n = 10,
# subset_rule = list(x = subset_vector),
# subsetable = TRUE
# )
#
# 2. The data variable is defined inside the function and no need to import other variables.
#
# anno2 = AnnotationFunction(
# fun = function(index) {
# x = 1:10
# n = length(index)
# pushViewport(viewport())
# grid.points(1:n, x[index])
# popViewport()
# },
# n = 10,
# subsetable = TRUE
# )
#
|
1a56796e |
# 3. Only specify the function to the constructor. ``anno3`` is not subsettable.
|
ad35494a |
#
# anno3 = AnnotationFunction(
# fun = function(index) {
# x = 1:10
# n = length(index)
# pushViewport(viewport())
# grid.points(1:n, x[index])
# popViewport()
# }
# )
|
e27480b9 |
#
|
1a56796e |
# As you can see from the examples, you need to push a viewport for graphics and finally pop the viewport.
|
ad35494a |
#
|
1a56796e |
# In the package, we have implemted quite a lot annotation function by `AnnotationFunction` constructor:
|
ad35494a |
# `anno_empty`, `anno_image`, `anno_points`, `anno_lines`, `anno_barplot`, `anno_boxplot`, `anno_histogram`,
|
1a56796e |
# `anno_density`, `anno_joyplot`, `anno_horizon`, `anno_text` and `anno_mark`. These built-in annotation functions
# support as both row annotations and column annotations and they are are all subsettable.
#
# == seealso
# The build-in annotation functions are already enough for most of the analysis, nevertheless, if users
# want to know more about how to construct the AnnotationFunction class manually, they can refer to
# ComplexHeatmap Complete Reference ().
|
261bfec2 |
AnnotationFunction = function(fun, fun_name = "", which = c("column", "row"),
|
c4a66bf9 |
var_import = list(), n = NA, data_scale = c(0, 1), subset_rule = list(),
|
1a56796e |
subsetable = length(subset_rule) > 0, show_name = TRUE, width = NULL, height = NULL) {
|
261bfec2 |
which = match.arg(which)[1]
|
1ee53830 |
verbose = ht_opt$verbose
|
402ff791 |
|
261bfec2 |
anno = new("AnnotationFunction")
anno@which = which
anno@fun_name = fun_name
|
402ff791 |
if(verbose) qqcat("construct AnnotationFunction with '@{fun_name}()'\n")
|
261bfec2 |
anno_size = anno_width_and_height(which, width, height, unit(1, "cm"))
anno@width = anno_size$width
anno@height = anno_size$height
|
933e808c |
anno@show_name = show_name
|
261bfec2 |
anno@n = n
anno@data_scale = data_scale
|
ad35494a |
if(length(var_import)) {
anno@var_env = new.env()
if(is.character(var_import)) {
for(nm in var_import) {
anno@var_env[[nm]] = get(nm, envir = parent.frame())
}
} else if(inherits(var_import, "list")) {
if(is.null(names(var_import))) {
var_imported_nm = sapply(as.list(substitute(var_import))[-1], as.character)
names(var_import) = var_imported_nm
}
|
261bfec2 |
|
ad35494a |
for(nm in names(var_import)) {
anno@var_env[[nm]] = var_import[[nm]]
}
} else {
|
c4a66bf9 |
stop_wrap("`var_import` needs to be a character vector which contains variable names or a list of variables.")
|
261bfec2 |
}
|
ad35494a |
environment(fun) = anno@var_env
|
261bfec2 |
} else {
|
ad35494a |
anno@var_env = environment(fun)
}
if(length(as.list(formals(fun))) == 1) {
formals(fun) = alist(index = , k = 1, n = 1)
|
261bfec2 |
}
anno@fun = fun
if(is.null(subset_rule)) {
for(nm in names(anno@var_env)) {
if(is.matrix(anno@var_env[[nm]])) {
anno@subset_rule[[nm]] = subset_matrix_by_row
} else if(inherits(anno@var_env[[nm]], "gpar")) {
anno@subset_rule[[nm]] = subset_gp
} else if(is.vector(anno@var_env[[nm]])) {
if(length(anno@var_env[[nm]]) > 1) {
anno@subset_rule[[nm]] = subset_vector
}
}
}
} else {
for(nm in names(subset_rule)) {
anno@subset_rule[[nm]] = subset_rule[[nm]]
}
}
if(missing(subsetable)) {
# is user defined subset rule
if(length(anno@subset_rule)) {
anno@subsetable = TRUE
}
} else {
anno@subsetable = subsetable
}
return(anno)
}
|
10fbf31c |
# == title
# Subset an AnnotationFunction Object
#
# == param
# -x An `AnnotationFunction-class` object.
|
e27480b9 |
# -i A vector of indices.
|
10fbf31c |
#
# == details
|
1a56796e |
# One good thing for designing the `AnnotationFunction-class` is it can be subsetted,
|
ad35494a |
# and this is the base for the splitting of the annotations.
|
10fbf31c |
#
# == example
# anno = anno_simple(1:10)
# anno[1:5]
# draw(anno[1:5], test = "subset of column annotation")
|
783c77fd |
"[.AnnotationFunction" = function(x, i) {
if(nargs() == 1) {
return(x)
} else {
|
261bfec2 |
if(!x@subsetable) {
|
c4a66bf9 |
stop_wrap("This object is not subsetable.")
|
261bfec2 |
}
|
783c77fd |
x = copy_all(x)
for(var in names(x@subset_rule)) {
|
261bfec2 |
oe = try(x@var_env[[var]] <- x@subset_rule[[var]](x@var_env[[var]], i), silent = TRUE)
if(inherits(oe, "try-error")) {
message(paste0("An error when subsetting ", var))
|
c4a66bf9 |
stop_wrap(oe)
|
261bfec2 |
}
|
783c77fd |
}
if(is.logical(i)) {
x@n = sum(i)
} else {
x@n = length(i)
}
return(x)
}
}
|
10fbf31c |
# == title
|
ad35494a |
# Draw the AnnotationFunction Object
|
10fbf31c |
#
# == param
# -object The `AnnotationFunction-class` object.
# -index Index of observations.
|
ad35494a |
# -k Current index of slice.
# -n Total number of slices.
|
10fbf31c |
# -test Is it in test mode? The value can be logical or a text which is plotted as the title of plot.
#
# == detail
# Normally it is called internally by the `SingleAnnotation` class.
#
# When ``test`` is set to ``TRUE``, the annotation graphic is directly drawn,
# which is generally for testing purpose.
#
|
783c77fd |
setMethod(f = "draw",
signature = "AnnotationFunction",
|
ad35494a |
definition = function(object, index, k = 1, n = 1, test = FALSE) {
|
783c77fd |
if(is.character(test)) {
test2 = TRUE
} else {
test2 = test
|
e84ad9b4 |
test = ""
|
783c77fd |
}
if(test2) {
grid.newpage()
|
261bfec2 |
pushViewport(viewport(width = 0.8, height = 0.8))
|
b8dacfcc |
if(is.na(object@n)) {
object@n = 1
}
|
783c77fd |
}
|
1ee53830 |
verbose = ht_opt$verbose
|
402ff791 |
if(verbose) qqcat("draw annotation generated by @{object@fun_name}\n")
|
783c77fd |
if(missing(index)) index = seq_len(object@n)
anno_height = object@height
anno_width = object@width
|
402ff791 |
|
783c77fd |
# names should be passed to the data viewport
pushViewport(viewport(width = anno_width, height = anno_height))
|
e27480b9 |
vp_name1 = current.viewport()$name
|
ad35494a |
object@fun(index, k, n)
|
783c77fd |
if(test2) {
grid.text(test, y = unit(1, "npc") + unit(2, "mm"), just = "bottom")
|
261bfec2 |
if(!identical(unit(0, "mm"), object@extended[1])) {
grid.rect(y = 1, height = unit(1, "npc") + object@extended[1], just = "top",
gp = gpar(fill = "transparent", col = "red", lty = 2))
} else if(!identical(unit(0, "mm"), object@extended[[2]])) {
grid.rect(x = 1, width = unit(1, "npc") + object@extended[2], just = "right",
gp = gpar(fill = "transparent", col = "red", lty = 2))
} else if(!identical(unit(0, "mm"), object@extended[[3]])) {
grid.rect(y = 0, height = unit(1, "npc") + object@extended[3], just = "bottom",
gp = gpar(fill = "transparent", col = "red", lty = 2))
} else if(!identical(unit(0, "mm"), object@extended[[4]])) {
grid.rect(x = 0, width = unit(1, "npc") + object@extended[4], just = "left",
gp = gpar(fill = "transparent", col = "red", lty = 2))
}
|
783c77fd |
}
|
e27480b9 |
vp_name2 = current.viewport()$name
if(vp_name1 != vp_name2) {
|
c4a66bf9 |
stop_wrap("Viewports should be the same before and after plotting the annotation graphics.")
|
e27480b9 |
}
|
783c77fd |
popViewport()
if(test2) {
popViewport()
}
})
|
10fbf31c |
# == title
|
1a56796e |
# Copy the AnnotationFunction Object
|
10fbf31c |
#
# == param
# -object The `AnnotationFunction-class` object.
#
|
1a56796e |
# == detail
# In `AnnotationFunction-class`, there is an environment which
# stores some external variables for the annotation function (specified by the
# ``var_import`` argument by constructing the `AnnotationFunction-class`
# object. This `copy_all,AnnotationFunction-method` hard copies all the
# variables into a new isolated environment.
|
ad35494a |
#
# The environment is at ``object@var_env``.
|
1a56796e |
#
|
783c77fd |
setMethod(f = "copy_all",
signature = "AnnotationFunction",
|
10fbf31c |
definition = function(object) {
|
783c77fd |
object2 = object
object2@var_env = new.env()
for(var in names(object@var_env)) {
object2@var_env[[var]] = object@var_env[[var]]
}
environment(object2@fun) = object2@var_env
return(object2)
})
|
10fbf31c |
# == title
|
1a56796e |
# Print the AnnotationFunction Object
|
10fbf31c |
#
# == param
# -object The `AnnotationFunction-class` object.
#
|
783c77fd |
setMethod(f = "show",
signature = "AnnotationFunction",
definition = function(object) {
|
261bfec2 |
cat("An AnnotationFunction object\n")
if(object@fun_name == "") {
cat(" function: user-defined\n")
} else {
cat(" function: ", object@fun_name, "()\n", sep = "")
}
|
783c77fd |
cat(" position:", object@which, "\n")
cat(" items:", ifelse(object@n == 0, "unknown", object@n), "\n")
|
261bfec2 |
cat(" width:", as.character(object@width), "\n")
cat(" height:", as.character(object@height), "\n")
|
d7a3c7af |
var_imported = names(object@var_env)
|
261bfec2 |
if(length(var_imported)) {
cat(" imported variable:", paste(var_imported, collapse = ", "), "\n")
|
d7a3c7af |
var_subsetable = names(object@subset_rule)
|
261bfec2 |
if(length(var_subsetable)) {
cat(" subsetable variable:", paste(var_subsetable, collapse = ", "), "\n")
}
}
cat(" this object is", ifelse(object@subsetable, "\b", "not"), "subsetable\n")
dirt = c("bottom", "left", "top", "right")
for(i in 1:4) {
if(!identical(unit(0, "mm"), object@extended[i])) {
cat(" ", as.character(object@extended[i]), "extension on the", dirt[i], "\n")
}
}
|
783c77fd |
})
|
10fbf31c |
# == title
# Number of Observations
#
# == param
|
ad35494a |
# -object The `AnnotationFunction-class` object.
|
1a56796e |
# -... Other arguments.
|
10fbf31c |
#
|
1a56796e |
# == details It returns the ``n`` slot in the object. If it does not exist, it
# returns ``NA``.
|
ad35494a |
#
# == example
# anno = anno_points(1:10)
# nobs(anno)
nobs.AnnotationFunction = function(object, ...) {
|
c4a66bf9 |
if(is.na(object@n)) {
return(NA)
} else if(object@n > 0) {
|
ad35494a |
object@n
|
261bfec2 |
} else {
|
402ff791 |
NA
|
783c77fd |
}
}
|