#' Plot genomic range elements in a pileup or collapsed format
#'
#' @usage plotRanges(
#' data,
#' chrom,
#' chromstart = NULL,
#' chromend = NULL,
#' assembly = "hg38",
#' fill = "#7ecdbb",
#' linecolor = NA,
#' order = "width",
#' collapse = FALSE,
#' boxHeight = unit(2, "mm"),
#' spaceWidth = 0.02,
#' spaceHeight = 0.3,
#' limitLabel = TRUE,
#' strandSplit = FALSE,
#' bg = NA,
#' baseline = FALSE,
#' baseline.color = "grey",
#' baseline.lwd = 1,
#' x = NULL,
#' y = NULL,
#' width = NULL,
#' height = NULL,
#' just = c("left", "top"),
#' default.units = "inches",
#' draw = TRUE,
#' params = NULL,
#' ...
#' )
#'
#' @param data Data to be plotted; as a character value specifying
#' a BED file path, a data frame in BED format, a character value
#' specifying a .bam file path where a bam index file (.bam.bai)
#' is in the same directory, or a \link[GenomicRanges]{GRanges} object.
#' @param chrom Chromosome of region to be plotted, as a string.
#' @param chromstart Integer start position on chromosome to be plotted.
#' @param chromend Integer end position on chromosome to be plotted.
#' @param assembly Default genome assembly as a string or a
#' \link[plotgardener]{assembly} object.
#' Default value is \code{assembly = "hg38"}.
#' @param fill A single character value, a vector, or a
#' \link[plotgardener]{colorby} object specifying fill colors of range elements.
#' Default value is \code{fill = "#7ecdbb"}.
#' @param linecolor A single character value, a vector, or a
#' \link[plotgardener]{colorby} object specifying the color of the lines
#' outlining range elements. Default value is \code{linecolor = NA}.
#' Special options include:
#' \itemize{
#' \item{\code{NA}: }{No line color.}
#' \item{\code{"fill"}: }{Same color as \code{fill}.}
#' } .
#' @param order A character value specifying how to order pileup data
#' before assigning rows. Default value is \code{order = "width"}. Options
#' include:
#' \itemize{
#' \item{\code{"width"}: }{Ordered by decreasing width of elements.}
#' \item{\code{"random"}: }{Ordered randomly in each function call.}
#' } .
#' @param collapse A logical value indicating whether to collapse
#' range elements into a single row, or into
#' two rows if \code{strandSplit = TRUE}.
#' If \code{collapse = TRUE}, \code{boxHeight} will be ignored and elements
#' will be the height of the entire plot if \code{strandSplit = FALSE} or
#' be the height of half of the entire plot if \code{strandSplit = TRUE}.
#' Default value is \code{collapse = FALSE}.
#' @param boxHeight A numeric or unit object specifying height of range element
#' boxes. Default value is \code{boxHeight = unit(2, "mm")}.
#' @param spaceWidth A numeric value specifying the width of minimum spacing
#' between range element boxes, as a fraction of the plot's genomic range.
#' Default value is \code{spaceWidth = 0.02}.
#' @param spaceHeight A numeric value specifying the height of spacing between
#' range element boxes on different rows, as a fraction of boxHeight.
#' Default value is \code{spaceHeight = 0.3}.
#' @param limitLabel A logical value indicating whether to draw a "+"
#' when not all elements can be plotted in the plotting space. Default
#' value is \code{limitLabel = TRUE}.
#' @param strandSplit A logical value indicating whether plus and
#' minus-stranded elements should be separated. Elements can only be
#' split by strand if a \code{strand} column is found in \code{data}.
#' Default value is \code{strandSplit = FALSE}.
#' @param bg Character value indicating background color.
#' Default value is \code{bg = NA}.
#' @param baseline Logical value indicating whether to include a
#' baseline along the x-axis. Default value is \code{baseline = FALSE}.
#' @param baseline.color Baseline color.
#' Default value is \code{baseline.color = "grey"}.
#' @param baseline.lwd Baseline line width.
#' Default value is \code{baseline.lwd = 1}.
#' @param x A numeric or unit object specifying ranges plot x-location.
#' @param y A numeric, unit object, or character containing a "b"
#' combined with a numeric value specifying ranges plot y-location.
#' The character value will
#' place the ranges plot y relative to the bottom of the most recently
#' plotted plot according to the units of the plotgardener page.
#' @param width A numeric or unit object specifying ranges plot width.
#' @param height A numeric or unit object specifying ranges plot height.
#' @param just Justification of ranges plot relative to its (x, y) location.
#' If there are two values, the first value specifies horizontal
#' justification and the second value specifies vertical justification.
#' Possible string values are: \code{"left"}, \code{"right"},
#' \code{"centre"}, \code{"center"}, \code{"bottom"}, and \code{"top"}.
#' Default value is \code{just = c("left", "top")}.
#' @param default.units A string indicating the default units to use
#' if \code{x}, \code{y}, \code{width}, or \code{height} are only given
#' as numerics. Default value is \code{default.units = "inches"}.
#' @param draw A logical value indicating whether graphics output should
#' be produced. Default value \code{draw = TRUE}.
#' @param params An optional \link[plotgardener]{pgParams} object
#' containing relevant function parameters.
#' @param ... Additional grid graphical parameters. See \link[grid]{gpar}.
#'
#' @return Returns a \code{ranges} object containing relevant
#' genomic region, coloring data, placement, and \link[grid]{grob} information.
#'
#' @examples
#' ## Load ranges data in BED format
#' library(plotgardenerData)
#' data("IMR90_ChIP_CTCF_reads")
#'
#' ## Create page
#' pageCreate(width = 7.5, height = 5, default.units = "inches")
#'
#' ## Plot and place a pileup ranges plot
#' pileupPlot <- plotRanges(
#' data = IMR90_ChIP_CTCF_reads, chrom = "chr21",
#' chromstart = 29073000, chromend = 29074000,
#' assembly = "hg19",
#' order = "random",
#' fill = colorby("strand", palette =
#' colorRampPalette(c("#7ecdbb", "#37a7db"))),
#' strandSplit = TRUE,
#' x = 0.5, y = 0.25, width = 6.5, height = 4.25,
#' just = c("left", "top"), default.units = "inches"
#' )
#'
#' ## Annotate genome label
#' annoGenomeLabel(
#' plot = pileupPlot, x = 0.5, y = 4.5,
#' just = c("left", "top")
#' )
#'
#' ## Add text labels
#' plotText(
#' label = "+ strand", fontcolor = "#37a7db", fontsize = 12,
#' x = 0.5, y = 1.25, just = "left"
#' )
#' plotText(
#' label = "- strand", fontcolor = "#7ecdbb", fontsize = 12,
#' x = 0.5, y = 3.5, just = "left"
#' )
#'
#' ## Hide page guides
#' pageGuideHide()
#' @details
#' A ranges plot can be placed on a plotgardener coordinate page by providing
#' plot placement parameters:
#' \preformatted{
#' plotRanges(data, chrom,
#' chromstart = NULL, chromend = NULL,
#' x, y, width, height, just = c("left", "top"),
#' default.units = "inches")
#' }
#' This function can also be used to quickly plot an unannotated BED plot
#' by ignoring plot placement parameters:
#' \preformatted{
#' plotRanges(data, chrom,
#' chromstart = NULL, chromend = NULL)
#' }
#'
#' @export
plotRanges <- function(data, chrom, chromstart = NULL, chromend = NULL,
assembly = "hg38", fill = "#7ecdbb",
linecolor = NA, order = "width", collapse = FALSE,
boxHeight = unit(2, "mm"), spaceWidth = 0.02,
spaceHeight = 0.3, limitLabel = TRUE,
strandSplit = FALSE, bg = NA,
baseline = FALSE, baseline.color = "grey",
baseline.lwd = 1,
x = NULL, y = NULL, width = NULL, height = NULL,
just = c("left", "top"), default.units = "inches",
draw = TRUE, params = NULL, ...) {
# =========================================================================
# FUNCTIONS
# =========================================================================
## Define a function that catches errors
errorcheck_plotPileup <- function(pileupPlot, fill, bed, order) {
## Genomic region
regionErrors(chromstart = pileupPlot$chromstart,
chromend = pileupPlot$chromend)
## Fill/colorby checks
checkColorby(fill = fill,
colorby = TRUE,
data = bed)
## Check order parameter options
if (!order %in% c("width", "random")){
stop("Invalid `order` parameter.", call. = FALSE)
}
}
## Define a function that parses the yscale based on split strands
strand_scale <- function(strandSplit, height) {
if (strandSplit == TRUE) {
yscale <- c(-height / 2, height / 2)
} else {
yscale <- c(0, height)
}
return(yscale)
}
## Define a function that orders input data based on "order" parameter
orderData <- function(data, order){
if (order == "width"){
# Order data by decreasing width of elements
orderedData <- data[order(data[, "width"], decreasing = TRUE), ]
} else if (order == "random"){
# Order data randomly
orderedData <- data[sample(nrow(data)), ]
}
return(orderedData)
}
# =========================================================================
# PARSE PARAMETERS
# =========================================================================
pileInternal <- parseParams(
params = params,
defaultArgs = formals(eval(match.call()[[1]])),
declaredArgs = lapply(match.call()[-1], eval.parent, n = 2),
class = "pileInternal"
)
## Parse gp
pileInternal$gp <- setGP(
gpList = gpar(),
params = pileInternal, ...
)
## Justification
pileInternal$just <- justConversion(just = pileInternal$just)
# =========================================================================
# CHECK ARGUMENT ERROS
# =========================================================================
if (is.null(pileInternal$data)) stop("argument \"data\" is missing, ",
"with no default.", call. = FALSE)
if (is.null(pileInternal$chrom)) stop("argument \"chrom\" is missing, ",
"with no default.", call. = FALSE)
# =========================================================================
# INITIALIZE OBJECT
# =========================================================================
pileupPlot <- structure(list(
chrom = pileInternal$chrom,
chromstart = pileInternal$chromstart,
chromend = pileInternal$chromend,
assembly = pileInternal$assembly,
color_palette = NULL,
zrange = NULL,
x = pileInternal$x, y = pileInternal$y,
width = pileInternal$width,
height = pileInternal$height,
just = pileInternal$just, grobs = NULL
),
class = "ranges"
)
attr(x = pileupPlot, which = "plotted") <- pileInternal$draw
# =========================================================================
# CHECK PLACEMENT ERRORS
# =========================================================================
check_placement(object = pileupPlot)
# =========================================================================
# PARSE ASSEMBLY
# =========================================================================
pileupPlot$assembly <- parseAssembly(assembly = pileupPlot$assembly)
# =========================================================================
# PARSE UNITS
# =========================================================================
pileupPlot <- defaultUnits(
object = pileupPlot,
default.units = pileInternal$default.units
)
pileInternal$boxHeight <- misc_defaultUnits(
value = pileInternal$boxHeight,
name = "boxHeight",
default.units = pileInternal$default.units
)
# =========================================================================
# READ IN FILE OR DATAFRAME
# =========================================================================
bed <- read_rangeData(data = pileInternal$data,
assembly = pileupPlot$assembly,
chrom = pileupPlot$chrom,
start = pileupPlot$chromstart,
end = pileupPlot$chromend)
# =========================================================================
# CATCH ERRORS
# =========================================================================
errorcheck_plotPileup(
pileupPlot = pileupPlot,
fill = pileInternal$fill,
bed = bed,
order = pileInternal$order
)
## chrom format and data chrom format
chromDataAgreement(data = bed, chrom = pileupPlot$chrom,
type = "ranges")
# =========================================================================
# GENOMIC SCALE
# =========================================================================
scaleChecks <- genomicScale(object = pileupPlot,
objectInternal = pileInternal,
plotType = "ranges plot")
pileupPlot <- scaleChecks[[1]]
pileInternal <- scaleChecks[[2]]
# =========================================================================
# COLORS
# =========================================================================
rangeColors <- parseColors(data = bed,
fill = pileInternal$fill,
object = pileupPlot,
subset = "ranges")
if (length(rangeColors[[1]]) > 0){
bed$color <- rangeColors[[1]]
} else {
bed$color <- rep("#7ecdbb", nrow(bed))
}
pileupPlot <- rangeColors[[2]]
bed$linecolor <- lineColors(linecolor = pileInternal$linecolor,
fillcolors = bed$color,
data = bed,
object = pileupPlot,
subset = "ranges")
# =========================================================================
# SUBSET DATA FOR CHROMOSOME AND ANY OVERLAPPING REGIONS
# =========================================================================
if (!is.null(pileupPlot$chromstart) & !is.null(pileupPlot$chromend)) {
bed <- bed[which(bed[, "chrom"] == pileupPlot$chrom
& bed[, "start"] <=
pileupPlot$chromend & bed[, "end"] >=
pileupPlot$chromstart), ]
} else {
bed <- data.frame(matrix(nrow = 0, ncol = 3))
}
## Add width column
bed$width <- bed[, "end"] - bed[, "start"]
# =========================================================================
# SEPARATE DATA INTO STRANDS
# =========================================================================
if (pileInternal$strandSplit == TRUE) {
## Look for column named 'strand'
if (!any(colnames(bed) == "strand")) {
stop("No `strand` column found in data. ",
"Cannot split data based on strand.", call. = FALSE)
} else {
posStrand <- bed[which(bed[, "strand"] == "+"), ]
minStrand <- bed[which(bed[, "strand"] == "-"), ]
}
}
# =========================================================================
# VIEWPORTS
# =========================================================================
## If placing information is provided but plot == TRUE,
## set up it's own viewport
## Not translating into page_coordinates
if (is.null(pileupPlot$x) | is.null(pileupPlot$y)) {
yscale <- strand_scale(
strandSplit = pileInternal$strandSplit,
height = 0.5
)
vp <- viewport(
height = unit(0.5, "snpc"), width = unit(1, "snpc"),
x = unit(0.5, "npc"), y = unit(0.5, "npc"),
clip = "on",
xscale = pileInternal$xscale,
yscale = yscale,
just = "center",
name = "ranges1"
)
if (pileInternal$draw == TRUE) {
grid.newpage()
}
} else {
## Get viewport name
currentViewports <- current_viewports()
vp_name <- paste0(
"ranges",
length(grep(
pattern = "ranges",
x = currentViewports
)) + 1
)
addViewport(vp_name)
## Convert coordinates into same units as page
page_coords <- convert_page(object = pileupPlot)
yscale <- strand_scale(
strandSplit = pileInternal$strandSplit,
height = convertHeight(page_coords$height,
unitTo = get("page_units",
envir = pgEnv
),
valueOnly = TRUE
)
)
## Make viewport
vp <- viewport(
height = page_coords$height, width = page_coords$width,
x = page_coords$x, y = page_coords$y,
clip = "on",
xscale = pileInternal$xscale,
yscale = yscale,
just = pileInternal$just,
name = vp_name
)
}
# =========================================================================
# INITIALIZE GTREE FOR GROBS WITH BACKGROUND
# =========================================================================
backgroundGrob <- rectGrob(
gp = gpar(
fill = pileInternal$bg,
col = NA
),
name = "background"
)
assign("pileup_grobs", gTree(
vp = vp,
children = gList(backgroundGrob)
),
envir = pgEnv
)
# =========================================================================
# DETERMINE ROWS FOR EACH ELEMENT
# =========================================================================
if (is.null(pileupPlot$x) & is.null(pileupPlot$y)) {
pushViewport(vp)
boxHeight <- convertHeight(pileInternal$boxHeight,
unitTo = "npc", valueOnly = TRUE
)
spaceHeight <- boxHeight * (pileInternal$spaceHeight)
upViewport()
} else {
boxHeight <- convertHeight(pileInternal$boxHeight,
unitTo = get("page_units",
envir = pgEnv
),
valueOnly = TRUE
)
spaceHeight <- boxHeight * (pileInternal$spaceHeight)
}
if (pileInternal$collapse == FALSE) {
## Calculate number of element rows that will fit
maxRows <- floor((as.numeric(vp$height) + spaceHeight) /
(boxHeight + spaceHeight))
wiggle <- abs(pileupPlot$chromend - pileupPlot$chromstart) *
pileInternal$spaceWidth
if (pileInternal$strandSplit == FALSE) {
## Order data
bed <- orderData(data = bed, order = pileInternal$order)
## Assign rows
rowData <- assignRows(data = bed[,c("start", "end")],
maxRows = maxRows,
wiggle = wiggle, rowCol = 2,
limitLabel = pileInternal$limitLabel,
gTree = "pileup_grobs",
extraData = bed[,c("color", "linecolor")],
colNames = c("color", "linecolor"))
## Calculate y-coordinates
rowData$y <- rowData$row * (boxHeight + spaceHeight)
} else {
## Order data
posStrand <- orderData(data = posStrand, order = pileInternal$order)
minStrand <- orderData(data = minStrand, order = pileInternal$order)
## Assign rows
posData <- assignRows(data = posStrand[,c("start", "end")],
maxRows = maxRows * 0.5,
wiggle = wiggle, rowCol = 2,
limitLabel = pileInternal$limitLabel,
gTree = "pileup_grobs",
extraData = posStrand[,c("color", "linecolor")],
colNames = c("color", "linecolor"))
minData <- assignRows(data = minStrand[,c("start", "end")],
maxRows = maxRows * 0.5,
wiggle = wiggle, rowCol = 2,
limitLabel = pileInternal$limitLabel,
side = "bottom",
gTree = "pileup_grobs",
extraData = minStrand[,c("color", "linecolor")],
colNames = c("color", "linecolor"))
## Calculate y-coordinates
posData$y <- (0.5 * spaceHeight) + posData$row *
(boxHeight + spaceHeight)
minData$y <- ((0.5 * spaceHeight + boxHeight) + minData$row *
(boxHeight + spaceHeight)) * -1
## Combine plus and minus strand data
rowData <- rbind(posData, minData)
}
} else {
if (pileInternal$strandSplit == FALSE) {
boxHeight <- as.numeric(vp$height)
rowData <- bed
## y-coordinates all the same
rowData$y <- rep(0, nrow(rowData))
} else {
boxHeight <- (1 - spaceHeight) * as.numeric(vp$height) * 0.5
if (nrow(posStrand) > 0) {
posStrand$y <- vp$yscale[2] - boxHeight
} else {
posStrand <- data.frame()
}
if (nrow(minStrand) > 0) {
minStrand$y <- vp$yscale[1]
} else {
minStrand <- data.frame()
}
## Combine plus and minus strand data
rowData <- rbind(posStrand, minStrand)
}
}
# =====================================================================
# MAKE GROBS
# =====================================================================
if (nrow(rowData) > 0) {
rowData$width <- rowData[,"end"] - rowData[,"start"]
alpha <- 1
if ("alpha" %in% names(pileInternal$gp)) {
alpha <- pileInternal$gp$alpha
}
bedRects <- rectGrob(
x = rowData$start,
y = rowData$y,
width = rowData$width,
height = boxHeight,
just = c("left", "bottom"),
default.units = "native",
gp = gpar(
fill = rowData$color,
col = rowData$linecolor,
alpha = alpha
)
)
assign("pileup_grobs",
addGrob(
gTree = get("pileup_grobs", envir = pgEnv),
child = bedRects
),
envir = pgEnv
)
if ((pileInternal$strandSplit == TRUE |
pileInternal$baseline == TRUE) &
pileInternal$collapse == FALSE) {
lineGrob <- segmentsGrob(
x0 = unit(0, "npc"), x1 = unit(1, "npc"),
y0 = unit(0, "native"), y1 = unit(0, "native"),
gp = gpar(
col = pileInternal$baseline.color,
lwd = pileInternal$baseline.lwd
)
)
assign("pileup_grobs",
addGrob(
gTree = get("pileup_grobs", envir = pgEnv),
child = lineGrob
),
envir = pgEnv
)
}
} else {
if (pileInternal$txdbChecks == TRUE) {
warning("No range data to plot.", call. = FALSE)
}
}
# =========================================================================
# IF PLOT == TRUE, DRAW GROBS
# =========================================================================
if (pileInternal$draw == TRUE) {
grid.draw(get("pileup_grobs", envir = pgEnv))
}
# =========================================================================
# ADD GROBS TO OBJECT
# =========================================================================
pileupPlot$grobs <- get("pileup_grobs", envir = pgEnv)
# =========================================================================
# RETURN OBJECT
# =========================================================================
message("ranges[", vp$name, "]")
invisible(pileupPlot)
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.