Nothing
##
## jamba-plots.r
##
## plotSmoothScatter
## smoothScatterJam
## imageDefault
#' Smooth scatter plot with enhancements
#'
#' Produce scatter plot using point density instead of displaying
#' individual data points.
#'
#' This function intends to make several potentially customizable
#' features of `graphics::smoothScatter()` plots much easier
#' to customize. For example bandwidthN allows defining the number of
#' bandwidth steps used by the kernel density function, and importantly
#' bases the number of steps on the visible plot window, and not the range
#' of data, which can differ substantially. The `nbin` argument is related,
#' but is used to define the level of detail used in the image function,
#' which when plotting numerous smaller panels, can be useful to reduce
#' unnecessary visual details.
#'
#' This function also by default produces a raster image plot
#' with `useRaster=TRUE`, which adjusts the x- and y-bandwidth to
#' produce visually round density even when the x- and y-ranges
#' are very different.
#'
#' Comments:
#'
#' * `asp=1` will define an aspect ratio 1, meaning the x-axis and y-axis
#' units will be the same physical size in the output device.
#' When this is true, and `fillBackground=TRUE` the `xlim` and `ylim`
#' values follow logic for `plot.default()` and `plot.window()` such that
#' each axis will include at least the `xlim` and `ylim` ranges, with
#' additional range included in order to maintain the plot aspect ratio.
#' * When `asp`, and any of `xlim` or `ylim`, are defined, the data will
#' be "cropped" to respective `xlim` and `ylim` values as relevant,
#' after which the plot is drawn with the appropriate plot aspect ratio.
#' When `applyRangeCeiling=TRUE`, points outside the fixed `xlim` and `ylim`
#' range are fixed to the edge of the range, after which the plot is drawn
#' with the requested plot aspect ratio. It is recommended not to define
#' `xlim` and `ylim` when also defining `asp`.
#' * When `add=TRUE` the `xlim` and `ylim` values are already defined
#' by the plot device. It is recommended not to define `xlim` and `ylim`
#' when `add=TRUE`.
#'
#' @family jam plot functions
#'
#' @param x numeric vector, or data matrix with two or more columns.
#' @param y numeric vector, or if data is supplied via x as a matrix, y
#' is NULL.
#' @param bwpi `numeric` value indicating the bandwidth "per inch"
#' to scale the bandwidth based upon visual space available.
#' This argument is used to define `bandwidthN`, however `bwpi`
#' is only used when `bandwidthN=NULL`.
#' The bandwidth is used to define the 2-dimensional point density.
#' @param binpi `numeric` value indicating the number of bins "per inch",
#' to scale based upon visual space available.
#' This argument is used to define `nbin`, however `binpi`
#' is only used when `nbin=NULL`.
#' @param bandwidthN `integer` number of bandwidth steps to use across the
#' visible plot window. Note that this bandwidth differs from default
#' `graphics::smoothScatter()` in that it uses the visible
#' plot window instead of the data range, so if the plot window is not
#' sufficiently similar to the data range, the resulting smoothed
#' density will not be visibly distorted. This parameter also permits
#' display of higher (or lower) level of detail.
#' @param nbin `integer` number of bins to use when converting the kernel
#' density result (which uses bandwidthN above) into a usable image.
#' This setting is effectively the resolution of rendering the
#' bandwidth density in terms of visible pixels. For example
#' `nbin=256` will create 256 visible pixels wide and tall in each
#' plot panel; and `nbin=32` will create 32 visible pixels, with
#' lower detail which may be suitable for multi-panel plots.
#' To use a variable number of bins, try `binpi`.
#' @param expand `numeric` value indicating the fraction of the x-axis
#' and y-axis ranges to add to create an expanded range,
#' used when `add=FALSE`. The default `expand=c(0.04, 0.04)` mimics
#' the R base plot default which adds 4 percent total, therefore 2 percent
#' to each side of the visible range.
#' @param transFactor `numeric` value used by the default `transformation`
#' function, which effectively scales the density of points to
#' a reasonable visible distribution. This argument is a convenience
#' method to avoid having to type out the full `transformation` function.
#' @param transformation `function` which converts point density to a number,
#' typically related to square root or cube root transformation. Note
#' that the default uses `transFactor` but if a custom function is
#' supplied, it will not use `transFactor` unless specified.
#' @param xlim `numeric` x-axis range, or `NULL` to use the data range.
#' @param ylim `numeric` y-axis range, or `NULL` to use the data range.
#' @param xlab,ylab `character` labels for x- and y-axis, respectively.
#' @param nrpoints `integer` number of outlier datapoints to display,
#' as defined by `graphics::smoothScatter()`, however the default here
#' is `nrpoints=0` to avoid additional clutter in the output,
#' and because the default arguments `bwpi`, `binpi` usually indicate all
#' individual points.
#' @param colramp any input recognized by `getColorRamp()`:
#' * `character` vector with multiple colors
#' * `character` string length 1, with valid R color used to create
#' a linear color gradient
#' * `character` name of a known color gradient from `RColorBrewer`
#' or `viridis`
#' * `function` that itself produces vector of colors,
#' in the form `function(n)` where `n` defines the number of colors.
#' @param col `character` string with R color used when `nrpoints` is
#' non-zero, this color defines the color of those points.
#' @param doTest `logical` indicating whether to create a visual set of test
#' plots to demonstrate the utility of this function.
#' @param fillBackground `logical` indicating whether to fill the
#' background of the plot panel with the first color in `colramp`.
#' The default `fillBackground=TRUE` is useful since the plot panel
#' may be slightly wider than the range of data being displayed, and
#' when the first color in `colramp` is not the same as the plot device
#' background color.
#' Run a test using:
#' `plotSmoothScatter(doTest=TRUE, fillBackground=FALSE, colramp="viridis")`
#' and compare with:
#' `plotSmoothScatter(doTest=TRUE, colramp="viridis")`
#' @param naAction `character` string indicating how to handle NA values,
#' typically when x is NA and y is not NA, or vice versa. valid values:
#' \describe{
#' \item{"remove"}{ignore any points where either x or y are NA}
#' \item{"floor0"}{change any NA values to zero 0 for either x or y}
#' \item{"floor1"}{change any NA values to one 1 for either x or y}
#' }
#' The latter two options are useful when the desired plot should indicate
#' the presence of an NA value in either x or y, while also indicating the
#' the corresponding non-NA value in the opposing axis. The driving use
#' was plotting gene fold changes from two experiments, where the two
#' experiments may not have measured the same genes.
#' @param xaxt `character` value compatible with graphics::par(xaxt), used to control
#' the x-axis range, similar to its use in `plot()` generic functions.
#' @param yaxt `character` value compatible with graphics::par(yaxt), used to control
#' the y-axis range, similar to its use in `plot()` generic functions.
#' @param add `logical` whether to add to an existing active R plot, or create
#' a new plot window.
#' @param asp `numeric` with optional aspect ratio, as described in
#' `graphics::plot.window()`, where `asp=1` defines x- and y-axis
#' coordinate ranges such that distances between points are rendered
#' accurately. One data unit on the y-axis is equal in length to
#' `asp` multiplied by one data unit on the x-axis.
#' Notes:
#' * When `add=TRUE`, the value `asp` is ignored, because
#' the existing plot device is re-used.
#' * When `add=FALSE` and `asp` is defined with `numeric` value,
#' a new plot device is opened using `plot.window()`, and the `xlim`
#' and `ylim` values are passed to that function. As a result the
#' `graphics::par("usr")` values are used to define `xlim` and `ylim` for the
#' purpose of determining visible points, relevant to `applyRangeCeiling`.
#' @param applyRangeCeiling `logical` indicating how to handle points outside
#' the visible plot range. Valid values:
#' \describe{
#' \item{TRUE}{Points outside the viewing area are fixed to the
#' plot boundaries, in order to represent that there are additional
#' points outside the boundary. This setting is recommended when
#' the reasonable viewing area is smaller than the actual data,
#' for example to be consistent across plot panels, but where
#' you want to indicate that points may be outside the range.}
#' \item{FALSE}{Points outside the viewing area is not displayed,
#' with no special visual indication. This setting is useful when
#' data may contain a large number of points at `c(0, 0)` and the
#' density overwhelms the detail in the rest of the plot. In that
#' case setting `xlim=c(1e-10, 10)` and `applyRangeCeiling=FALSE`
#' would obscure these points.}
#' }
#' @param useRaster `logical` indicating whether to produce plots using the
#' `graphics::rasterImage()` function which produces a plot
#' raster image offline then scales this image to visible plot space.
#' This technique has two benefits:
#' 1. It produces substantially faster plot output.
#' 2. Output contains substantially fewer plot objects, which results
#' in much smaller file sizes when saving in 'PDF' or 'SVG' format.
#' @param verbose `logical` indicating whether to print verbose output.
#' @param ... additional arguments are passed to called functions,
#' including `getColorRamp()`, `nullPlot()`, `smoothScatterJam()`.
#'
#' @returns `list` invisibly, sufficient to reproduce most of the
#' graphical parameters used to create the smooth scatter plot.
#'
#' @examples
#' # doTest=TRUE invisibly returns the test data
#' x <- plotSmoothScatter(doTest=TRUE);
#'
#' # so it can be plotted again with different settings
#' colnames(x) <- c("column_1", "column_2")
#' plotSmoothScatter(x, colramp="RdBu_r");
#'
#' @export
plotSmoothScatter <- function
(x,
y=NULL,
bwpi=50,
binpi=50,
bandwidthN=NULL,
nbin=NULL,
expand=c(0.04, 0.04),
transFactor=0.25,
transformation=function(x)x^transFactor,
xlim=NULL,
ylim=NULL,
xlab=NULL,
ylab=NULL,
nrpoints=0,
colramp=c("white", "lightblue", "blue", "orange", "orangered2"),
col="black",
doTest=FALSE,
fillBackground=TRUE,
naAction=c("remove", "floor0", "floor1"),
xaxt="s",
yaxt="s",
add=FALSE,
asp=NULL,
applyRangeCeiling=TRUE,
useRaster=TRUE,
verbose=FALSE,
...)
{
## Purpose is to wrapper the graphics::smoothScatter() function in order to increase
## the apparent detail by adjusting the bandwidth parameters in a somewhat
## more automated/intuitive way than the default parameters.
## Now, the graphics::smoothScatter() function uses some default number, which results
## in a low amount of detail, and in my opinion loses important features.
## To see an example of the difference, and hopefully the visual improvement,
## run this function plotSmoothScatter(doTest=TRUE)
##
## fillBackground=TRUE will fill the plot with the background color
## (first color in colramp), which corrects the issue when the
## smoothScatter only returns data for part of the plot.
##
## dotest=TRUE will display some visual benefits of plotSmoothScatter()
##
## colramp is either a color function, or a vector or single color. If the
## class is "character" it is sent to getColorRamp() which returns a color
## ramp. It currently recognizes Brewer colors, viridis, or other
## combinations of colors.
##
## plotSmoothScatter(doTest=TRUE, colramp="viridis")
## plotSmoothScatter(doTest=TRUE, colramp="navy")
## plotSmoothScatter(doTest=TRUE, colramp=c("white","navy","orange")
##
naAction <- match.arg(naAction);
# make sure colramp is a function
if (length(colramp) == 0) {
colramp <- eval(formals(plotSmoothScatter)$colramp);
} else {
colramp <- getColorRamp(colramp,
n=NULL,
...);
}
# validate expand for x and y axis limit expansion
if (length(expand) == 0) {
expand <- c(0, 0);
}
expand <- rep(expand,
length.out=2);
# optional visual test
if (doTest) {
## create somewhat noisy correlation data
n <- 20000;
x <- matrix(ncol=2, data=stats::rnorm(n*2));
x[,2] <- x[,1] + stats::rnorm(n)*0.1;
## Add secondary line offset from the main correlation
## using 5% the original data
xSample <- sample(1:nrow(x), floor(nrow(x)*0.05));
xSub <- t(t(x[xSample,,drop=FALSE])+c(0.6,-0.7));
## Add more noise to a subset of data
n1 <- 3000;
x2 <- rbind(x, xSub);
n2 <- sample(seq_len(nrow(x2)), n1);
x2[n2,2] <- x2[n2,1] + stats::rnorm(n1) * 0.6;
withr::local_par(list("mfrow"=c(2,2),
"mar"=c(2, 3, 4, 1)));
graphics::smoothScatter(x2,
main="smoothScatter default\n(using colramp blues9)",
asp=asp,
ylab="",
xlab="");
plotSmoothScatter(x2,
colramp=colramp,
fillBackground=fillBackground,
main="plotSmoothScatter\ndefault",
asp=asp,
...);
plotSmoothScatter(x2,
colramp=c("white", grDevices::blues9),
fillBackground=fillBackground,
main="plotSmoothScatter\n(using colramp blues9)",
asp=asp,
...);
xy <- plotSmoothScatter(x2,
colramp=colramp,
bwpi=bwpi * 1.5,
bandwidthN=bandwidthN * 1.5,
binpi=binpi * 1.5,
fillBackground=fillBackground,
main="plotSmoothScatter\nwith increased bandwidth and bin",
asp=asp,
...);
return(invisible(x2));
}
## use grDevices::xy.coords()
xlabel <- if (!missing(x))
deparse(substitute(x));
ylabel <- if (length(y) > 0)
deparse(substitute(y));
xy <- grDevices::xy.coords(x=x,
y=y,
xlab=xlabel,
ylab=ylabel,
recycle=TRUE,
setLab=TRUE);
if (length(xlab) == 0) {
xlab <- xy$xlab;
}
if (length(ylab) == 0) {
ylab <- xy$ylab;
}
x <- xy$x;
y <- xy$y;
## Deal with NA values
if (naAction == "remove") {
naValues <- (is.na(x) | is.na(y));
x <- x[!naValues];
y <- y[!naValues];
} else if (naAction == "floor0") {
naValuesX <- is.na(x);
x[naValuesX] <- 0;
naValuesY <- is.na(y);
y[naValuesY] <- 0;
} else if (naAction == "floor1") {
naValuesX <- is.na(x);
x[naValuesX] <- 1;
naValuesY <- is.na(y);
y[naValuesY] <- 1;
}
if (length(xlim) == 0) {
if (TRUE %in% add && exists(".Devices")) {
xlim <- range(graphics::par("usr")[1:2], na.rm=TRUE);
} else {
xlim <- range(x, na.rm=TRUE);
}
}
if (length(ylim) == 0) {
if (TRUE %in% add && exists(".Devices")) {
ylim <- range(graphics::par("usr")[3:4], na.rm=TRUE);
} else {
ylim <- range(y, na.rm=TRUE);
}
}
## Apply a ceiling to values outside the range
if (applyRangeCeiling) {
tooHighX <- x > max(xlim);
tooLowX <- x < min(xlim);
x[tooHighX] <- max(xlim);
x[tooLowX] <- min(xlim);
tooHighY <- y > max(ylim);
tooLowY <- y < min(ylim);
y[tooHighY] <- max(ylim);
y[tooLowY] <- min(ylim);
}
# expand xlim and ylim viewing range
xlim4 <- sort((c(-1,1) * diff(xlim) * expand[1]/2) + xlim);
ylim4 <- sort((c(-1,1) * diff(ylim) * expand[2]/2) + ylim);
if (verbose) {
printDebug("plotSmoothScatter(): ",
"xlim: ", xlim4);
printDebug("plotSmoothScatter(): ",
"ylim: ", ylim4);
}
## Adjust for uneven plot aspect ratio, by using the plot graphics::par("pin")
## which contains the actual dimensions.
## Note that it does not require the actual coordinates of the plot,
## just the relative size of the display
if (!TRUE %in% add) {
if (TRUE %in% fillBackground) {
nullPlot(doBoxes=FALSE,
doUsrBox=FALSE,
fill=head(colramp(11),1),
xaxs="i",
yaxs="i",
xaxt="n",
yaxt="n",
xlim=xlim4,
ylim=ylim4,
add=add,
asp=asp,
# border="transparent",
...);
# use grid.rect since it scales when plot is resized
graphics::abline(h=mean(ylim4), col="transparent")
grid::grid.rect(gp=grid::gpar(
fill=head(colramp(11), 1)))
} else {
nullPlot(doBoxes=FALSE,
xaxs="i",
yaxs="i",
xaxt="n",
yaxt="n",
xlim=xlim4,
ylim=ylim4,
add=add,
asp=asp,
...);
}
if (length(asp) == 1) {
parUsr <- graphics::par("usr");
xlim4 <- parUsr[1:2]
ylim4 <- parUsr[3:4]
if (verbose) {
printDebug("plotSmoothScatter(): ",
"parUsr: ", parUsr);
printDebug("plotSmoothScatter(): ",
"xlim: ", xlim4);
printDebug("plotSmoothScatter(): ",
"ylim: ", ylim4);
}
}
graphics::axis(1, las=1, xaxt=xaxt);
graphics::axis(2, las=2, yaxt=yaxt);
if ((length(xlab) > 0 && nchar(xlab) > 0) ||
(length(ylab) > 0 && nchar(ylab) > 0)) {
graphics::title(xlab=xlab,
ylab=ylab,
...);
}
} else {
# add=TRUE
# so xlim,ylim are already defined
if (fillBackground) {
graphics::abline(h=mean(ylim4), col="transparent")
grid::grid.rect(
gp=grid::gpar(
fill=head(colramp(11), 1)))
}
parUsr <- graphics::par("usr");
xlim4 <- parUsr[1:2]
ylim4 <- parUsr[3:4]
if (verbose) {
printDebug("plotSmoothScatter(): ",
"parUsr: ", parUsr);
printDebug("plotSmoothScatter(): ",
"xlim: ", xlim4);
printDebug("plotSmoothScatter(): ",
"ylim: ", ylim4);
}
}
## Determine resolution of 2D density, and of pixel display
pin1 <- graphics::par("pin")[1] / graphics::par("pin")[2];
if (length(bandwidthN) > 0) {
bandwidthN <- rep(bandwidthN, length.out=2);
bandwidthXY <- c(diff(xlim4)/bandwidthN[1],
diff(ylim4)/bandwidthN[2]*pin1);
} else {
## Alternate method using breaks per inch
if (length(bwpi) == 0) {
bwpi <- 30;
}
bandwidthXY <- c(
diff(xlim4) / (graphics::par("pin")[1] * bwpi),
diff(ylim4) / (graphics::par("pin")[2] * bwpi));
}
if (length(nbin) == 0) {
if (length(binpi) == 0) {
binpi <- 50;
}
nbin <- c(
round(graphics::par("pin")[1] * binpi),
round(graphics::par("pin")[2] * binpi));
}
if (verbose) {
jamba::printDebug("plotSmoothScatter(): ",
"bandwidthXY: ",
bandwidthXY);
jamba::printDebug("nbin: ", nbin);
}
ssj <- smoothScatterJam(x=x,
y=y,
add=TRUE,
transformation=transformation,
bandwidth=bandwidthXY,
nbin=nbin,
nrpoints=nrpoints,
xlim=xlim4,
ylim=ylim4,
xaxs="i",
yaxs="i",
xaxt="n",
yaxt="n",
colramp=colramp,
col=col,
useRaster=useRaster,
...);
return(invisible(ssj));
invisible(list(x=x,
y=y,
transformation=transformation,
bandwidth=bandwidthXY,
nbin=nbin,
xlim=xlim4,
ylim=ylim4,
xaxs="i",
yaxs="i",
xaxt=xaxt,
yaxt=yaxt,
colramp=colramp,
nrpoints=nrpoints,
col=col));
}
#' Smooth scatter plot, Jam style
#'
#' Produce smooth scatter plot, a helper function called by
#' `plotSmoothScatter()`.
#'
#' For general purposes, use `plotSmoothScatter()` as a replacement
#' for `graphics::smoothScatter()`, which produces better default
#' settings for pixel size and density bandwidth.
#'
#' This function is only necessary in order to override the
#' `graphics::smoothScatter()` function which calls
#' `graphics::image.default()`.
#' Instead, this function calls `imageDefault()` which is required
#' in order to utilize custom raster image scaling, particularly important
#' when the x- and y-axis ranges are not similar, e.g. where the x-axis spans
#' 10 units, but the y-axis spans 10,000 units.
#'
#' @family jam internal functions
#'
#' @param x `numeric` vector, or data matrix with two or more columns.
#' @param y `numeric` vector, or if data is supplied via x as a matrix, y
#' is NULL.
#' @param nbin `integer` number of bins to use when converting the kernel
#' density result (which uses bandwidthN above) into a usable image.
#' For example, nbin=123 is the default used by
#' `graphics::smoothScatter()`, however the
#' `plotSmoothScatter()` function default is higher (256).
#' @param bandwidth `numeric` vector used to define the y- and x-axis
#' bandwidths, respectively, passed to `KernSmooth::bkde2D()`,
#' which calculates the underlying 2-dimensional kernel density.
#' The `plotSmoothScatter()` function was motivated by never wanting
#' to define this number directly, instead auto-calculation suitable
#' values.
#' @param colramp `function` that takes one `numeric` argument and returns
#' that integer number of colors, by default 256.
#' @param nrpoints `integer` number of outlier datapoints to display,
#' as defined by `graphics::smoothScatter()`, however the default here
#' is `nrpoints=0` to avoid additional clutter in the output,
#' and because the default argument `bandwidthN` usually indicates all
#' individual points.
#' @param pch `integer` point shape used when `nrpoints>0`.
#' @param cex `numeric` point size expansion factor used when `nrpoints>0`.
#' @param col `character` R color used when `nrpoints>0`.
#' @param transformation `function` which converts point density to a number,
#' typically related to square root or cube root transformation.
#' @param postPlotHook `function` or `NULL`, NULL default.
#' When `function` is supplied, it is called after producing the image.
#' By default it is simply used to draw a box around the image,
#' but could be used to layer additional information atop the image plot,
#' for example contours, labels, etc.
#' @param xlab `character` x-axis label
#' @param ylab `character` y-axis label
#' @param xlim `numeric` x-axis range for the plot
#' @param ylim `numeric` y-axis range for the plot
#' @param add `logical` whether to add to an existing active R plot, or create
#' a new plot window.
#' @param xaxs `character` value compatible with `graphics::par("xaxs")`, mainly useful
#' for suppressing the x-axis, in order to produce a custom x-axis
#' range, most useful to restrict the axis range expansion done by R
#' by default.
#' @param yaxs `character` value compatible with `graphics::par("yaxs")`, mainly useful
#' for suppressing the y-axis, in order to produce a custom y-axis
#' range, most useful to restrict the axis range expansion done by R
#' by default.
#' @param xaxt `character` value compatible with `graphics::par("xaxt")`, mainly useful
#' for suppressing the x-axis, in order to produce a custom x-axis
#' by other mechanisms, e.g. log-scaled x-axis tick marks.
#' @param yaxt `character` value compatible with `graphics::par("yaxt")`, mainly useful
#' for suppressing the y-axis, in order to produce a custom y-axis
#' by other mechanisms, e.g. log-scaled y-axis tick marks.
#' @param useRaster `NULL` or `logical` indicating whether to invoke
#' `graphics::rasterImage()` to produce a raster image.
#' If NULL, it determines whether to produce a raster image within the
#' `imageDefault()` function, which checks the options
#' using `getOption("preferRaster", FALSE)` to determine among
#' other things, whether the user prefers raster images, and if the
#' `grDevices::dev.capabilities()` supports raster.
#' @param ... additional arguments are passed to `imageDefault()` and
#' optionally to `plotPlotHook()` when supplied.
#'
#' @seealso `graphics::smoothScatter()`
#'
#' @returns `list` of elements sufficient to call `graphics::image()`,
#' also by default this function is called for the byproduct of
#' creating a figure.
#'
#' @examples
#' x1 <- rnorm(1000);
#' y1 <- (x1 + 5)* 4 + rnorm(1000);
#' smoothScatterJam(x=x1, y=y1, bandwidth=c(0.05, 0.3))
#'
#' @export
smoothScatterJam <- function
(x,
y=NULL,
nbin=256,
bandwidth,
colramp=grDevices::colorRampPalette(c("white",
"lightblue",
"blue",
"orange",
"orangered2")),
nrpoints=100,
pch=".",
cex=1,
col="black",
transformation=function(x) x^0.25,
postPlotHook=graphics::box,
xlab=NULL,
ylab=NULL,
xlim,
ylim,
add=FALSE,
xaxs=graphics::par("xaxs"),
yaxs=graphics::par("yaxs"),
xaxt=graphics::par("xaxt"),
yaxt=graphics::par("yaxt"),
useRaster=NULL,
...)
{
## Purpose is to wrapper the graphics::smoothScatter() function
## solely so it uses imageDefault, which enables improved
## useRaster=TRUE functionality
##
## postPlotHook is a function called after the plot is created,
## useful for drawing a box around the plot, or performing
## other customizations.
##
if (!is.numeric(nrpoints) | (nrpoints < 0) | (length(nrpoints) !=1)) {
stop("'nrpoints' should be numeric scalar with value >= 0.");
}
xlabel <- if (!missing(x)) {
deparse(substitute(x));
}
ylabel <- if (!missing(y)) {
deparse(substitute(y));
}
xy <- grDevices::xy.coords(x, y, xlabel, ylabel);
xlab <- if (is.null(xlab)) {
xy$xlab;
} else {
xlab;
}
ylab <- if (is.null(ylab)) {
xy$ylab;
} else {
ylab;
}
x <- cbind(xy$x, xy$y)[is.finite(xy$x) & is.finite(xy$y),,drop=FALSE];
if (!missing(xlim)) {
stopifnot(is.numeric(xlim), length(xlim) == 2, is.finite(xlim));
x <- x[min(xlim) <= x[, 1] & x[, 1] <= max(xlim),];
} else {
xlim <- range(x[, 1]);
}
if (!missing(ylim)) {
stopifnot(is.numeric(ylim), length(ylim) == 2, is.finite(ylim));
x <- x[min(ylim) <= x[, 2] & x[, 2] <= max(ylim),];
} else {
ylim <- range(x[, 2]);
}
# calculate point density
map <- jamCalcDensity(x=x,
nbin=nbin,
bandwidth=bandwidth);
xm <- map$x1;
ym <- map$x2;
dens <- map$fhat;
dens[] <- transformation(dens);
# initialize the plot
imageDefault(xm,
ym,
z=dens,
col=colramp(256),
xlab=xlab,
add=add,
ylab=ylab,
xlim=xlim,
ylim=ylim,
xaxs=xaxs,
yaxs=yaxs,
xaxt=xaxt,
yaxt=yaxt,
useRaster=useRaster,
...);
# create list with data used
imageL <- list(xm=xm,
ym=ym,
z=dens,
col=colramp(256),
xlab=xlab,
add=add,
ylab=ylab,
xlim=xlim,
ylim=ylim,
xaxs=xaxs,
yaxs=yaxs,
xaxt=xaxt,
yaxt=yaxt);
if (length(postPlotHook) > 0) {
if (igrepHas("function", class(postPlotHook))) {
postPlotHook(...);
} else {
postPlotHook;
}
}
if (nrpoints > 0) {
nrpoints <- min(nrow(x), ceiling(nrpoints));
stopifnot((nx <- length(xm)) == nrow(dens),
(ny <- length(ym)) == ncol(dens));
ixm <- 1L + as.integer((nx - 1) * (x[, 1] - xm[1])/(xm[nx] - xm[1]));
iym <- 1L + as.integer((ny - 1) * (x[, 2] - ym[1])/(ym[ny] - ym[1]));
sel <- order(dens[cbind(ixm, iym)])[seq_len(nrpoints)];
graphics::points(x[sel,, drop=FALSE],
pch=pch,
cex=cex,
col=col);
}
invisible(imageL);
}
#' Create a blank plot with optional labels
#'
#' Create a blank plot with optional labels for margins
#'
#' This function creates an empty plot space, using the current
#' `graphics::par()` settings for margins, text size, etc. By default
#' it displays a box around the plot window, and labels the margins and
#' plot area for review. It can be useful as a visual display of various
#' base graphics settings, or to create an empty plot window with pre-defined
#' axis ranges. Lastly, one can use this function to create a "blank" plot
#' which uses a defined background color, which can be a useful precursor to
#' drawing an image density which may not cover the whole plot space.
#'
#' @family jam plot functions
#'
#' @returns no output, this function is called for the byproduct of
#' creating a blank plot, optionally annotating the margins.
#'
#' @param xaxt `character` value compatible with`options("xaxt")`
#' @param yaxt `character` value compatible with `options("xaxt")`
#' @param xlab `character` x-axis label
#' @param ylab `character` y-axis label
#' @param col `character` colors passed to `plot()`
#' @param xlim `numeric` x-axis range
#' @param ylim `numeric` y-axis range
#' @param las `integer` value indicating whether axis labels should be
#' parallel (1) or perpendicular (2) to the axis line.
#' @param doBoxes `logical` whether to draw annotated boxes around the plot
#' and inner and outer margins.
#' @param doUsrBox `logical` whether to draw a colored bow indicating the
#' exact plot space, using the function `usrBox()`.
#' @param fill `character` R color used to fill the background of the plot
#' as used by `usrBox()`.
#' @param doAxes `logical` whether to draw default x- and y-axes.
#' @param doMargins `logical` whether to label margins, only active when
#' doBoxes=TRUE.
#' @param marginUnit `character` indicating the units used for margin labels.
#' @param plotAreaTitle `character` label printed in the center of the plot
#' area.
#' @param plotSrt numeric angle for the plotAreaTitle, which is good for
#' labeling this plot with vertical text when displaying a plot panel
#' inside a grid layout, where the plot is taller than it is wide.
#' @param plotNumPrefix `character` or integer label appended as suffix to
#' margin labels, which is useful when annotating multiple plots in a
#' grid layout, where labels are sometimes quite close together. This
#' label is but a simple attempt to sidestep the real problem of fitting
#' labels inside each visual component.
#' @param bty `character` passed `plot()`, default `"n"` suppresses
#' the default box, which can then be optionally drawn
#' based upon the `doBoxes` parameter.
#' @param showMarginsOnly `logical` whether to create a new plot or to annotate
#' an existing active plot.
#' @param add `logical` whether to add to an existing active R plot, or create
#' a new plot window.
#' @param ... additional arguments are ignored.
#'
#' @examples
#' nullPlot()
#'
#' nullPlot(doBoxes=FALSE)
#'
#' @export
nullPlot <- function
(xaxt="n",
yaxt="n",
xlab="",
ylab="",
col="transparent",
xlim=c(1,2),
ylim=c(1,2),
las=graphics::par("las"),
doBoxes=TRUE,
doUsrBox=doBoxes,
fill="#FFFF9966",
doAxes=FALSE,
doMargins=TRUE,
marginUnit=c("lines",
"inches"),
plotAreaTitle="Plot Area",
plotSrt=0,
plotNumPrefix="",
bty="n",
showMarginsOnly=FALSE,
add=FALSE,
...)
{
## Purpose is just to create a dead-simple plot of two points,
## so that when the plot window is resized, it does not hang the computer
## (as happens something when really intricate plots are resized)
##
## doBoxes=TRUE will also draw boxes around relevant features including
## plot region, margins, outer margins (if defined), and add labels.
##
## doUsrBox is a colored box with light yellow background covering the
## plot region, accomplished with usrBox(...).
##
## doMargins=TRUE will add text labels indicating various margin settings.
##
## plotAreaTitle text is positioned in the center of the plot region,
## which is good for labeling a plot region in a layout.
##
## plotSrt defines the angle of the plotAreaTitle, which is good for
## labeling this plot with vertical text when displaying a plot panel
## inside a grid layout, where the plot is taller than it is wide.
##
## doBoxes was adapted from Earl F. Glynn's margin tutorial:
## http://research.stowers.org/mcm/efg/R/Graphics/Basics/mar-oma/index.htm
## Update Aug2017: The link is now defunct, and no archive is available.
##
## showMarginsOnly=TRUE will not create a new plot, but
## instead just annotates margins on the existing plot
# validate arguments
marginUnit <- match.arg(marginUnit);
if (showMarginsOnly) {
parUsr <- graphics::par("usr");
ylim <- parUsr[3:4];
xlim <- parUsr[1:2];
graphics::points(range(xlim),
range(ylim),
xaxt=xaxt,
yaxt=yaxt,
col=col,
xlab=xlab,
ylab=ylab,
xlim=xlim,
ylim=ylim,
bty=bty,
add=TRUE,
...);
} else {
if (!add) {
plot(range(xlim),
range(ylim),
xaxt=xaxt,
yaxt=yaxt,
col=col,
xlab=xlab,
ylab=ylab,
xlim=xlim,
ylim=ylim,
bty=bty,
#add=add,
...);
}
}
if (doUsrBox) {
usrBox(fill=fill,
...);
}
if (doBoxes) {
graphics::box("plot",
col="darkred");
graphics::box("figure",
lty="dashed",
col="navy");
## Print margins
if (doMargins) {
if ("lines" %in% marginUnit) {
# lines
Margins <- graphics::par("mar");
MarginTerm <- "mar";
OMargins <- graphics::par("oma");
OMarginTerm <- "oma";
} else {
# inches
Margins <- graphics::par("mai");
MarginTerm <- "mai";
OMargins <- graphics::par("omi");
OMarginTerm <- "omi";
}
MarginsN <- Margins;
# Margins <- substr(Margins, 5, nchar(Margins));
# MarginsN <- as.numeric(unlist(strsplit(Margins, "[ ]+")));
MarginsV <- format(MarginsN,
nsmall=0,
scientific=FALSE,
digits=3);
OMarginsV <- format(OMargins,
nsmall=0,
scientific=FALSE,
digits=3);
MarginsText <- paste0(" ", MarginTerm,
"=c(", paste(MarginsV, collapse=", "), ")",
plotNumPrefix);
OMarginsText <- paste0(" ", OMarginTerm,
"=c(", paste(OMarginsV, collapse=", "), ")",
plotNumPrefix);
if (plotSrt == 90) {
plotLas <- 2;
} else {
plotLas <- 1;
}
if (MarginsN[3] < 1) {
graphics::mtext(paste0("Margin", MarginsText),
NORTH<-3,
line=-1,
cex=0.7,
col="navy",
las=plotLas,
adj=plotLas-1);
} else {
graphics::mtext(paste0("Margin", MarginsText),
SOUTH<-3,
line=1,
cex=0.7,
col="navy",
las=plotLas,
adj=plotLas-1);
}
graphics::box("inner", lty="dotted", col="darkgreen", lwd=3);
if (any(OMargins > 0)) {
if (OMargins[1] >= 1) {
graphics::mtext(paste0("Outer Margin Area", OMarginsText),
SOUTH<-1,
line=0,
adj=1.0,
cex=0.7,
col="darkgreen",
outer=TRUE,
las=plotLas);
} else {
graphics::mtext(paste0("Outer Margin Area", OMarginsText),
SOUTH<-1,
line=-1,
adj=1.0,
cex=0.7,
col="darkgreen",
outer=TRUE,
las=plotLas);
}
}
graphics::box("outer", lty="solid", col="darkgreen");
## Text: vector of strings in mtext call
lapply(1:4, function(i){
if (MarginsV[i] > 0) {
newLas <- as.integer(3 - i%%2*2);
withr::local_par("las"=newLas);
graphics::mtext(paste0(MarginTerm, "[", i, "]",
plotNumPrefix, "=",
MarginsV[i]),
side=i,
line=0.4,
cex=0.6,
col="navy",
outer=FALSE,
las=newLas);
}
});
lapply(1:4, function(i){
if (OMarginsV[i] > 0) {
newLas <- as.integer(3 - i%%2*2);
withr::local_par("las"=newLas);
graphics::mtext(paste0("oma[", i, "]",
plotNumPrefix, "=",
OMarginsV[i]),
i,
adj=0.5,
padj=0,
line=-0.1,
cex=0.6,
col="navy",
outer=TRUE,
las=newLas);
}
});
}
## Print a title in the center of the plot region
withr::local_par("xpd"=NA);
graphics::text(x=mean(range(xlim)),
y=mean(range(ylim)),
labels=plotAreaTitle,
col="darkred",
cex=2,
srt=plotSrt);
## Print axis labels
if (doAxes) {
graphics::axis(1,
las=las,
col="darkred",
col.axis="darkred",
...);
graphics::axis(2,
las=las,
col="darkred",
col.axis="darkred",
...);
}
}
}
#' Draw colored box indicating R plot space
#'
#' Draw colored box indicating the active R plot space
#'
#' This function simply draws a box indicating the active plot space,
#' and by default it shades the box light yellow with transparency. It
#' can be useful to indicate the active plot area while allowing pre-drawn
#' plot elements to be shown, or can be useful precursor to provide a colored
#' background for the plot.
#'
#' The plot space is defined using \code{graphics::par("usr")} and therefore requires
#' an active R device is already opened.
#'
#' @param fill `character` R color used to fill the background of the plot
#' @param label `character` text optionally used to label the center of the
#' plot space, default `NULL`
#' @param parUsr `numeric` vector length 4, indicating the R plot space,
#' consistent with \code{graphics::par("usr")}. It can thus be used to define a
#' different area, though using the \code{\link[graphics]{rect}} function
#' directly seems more appropriate.
#' @param debug `logical` whether to print the parUsr value being used.
#' @param ... additional arguments are ignored.
#'
#' @family jam plot functions
#'
#' @returns no output, this function is called for the byproduct
#' of adding a box in the usr plot space of an R graphics device.
#'
#' @examples
#' # usrBox() requires that a plot device is already open
#' nullPlot(doBoxes=FALSE);
#' usrBox();
#'
#' @export
usrBox <- function
(fill="#FFFF9966",
label=NULL,
parUsr=graphics::par("usr"),
debug=FALSE,
...)
{
## Purpose is to draw a rectangle, filled transparent yellow,
## showing the graphics::par("usr") area as defined by R.
## This function can also be used to change the plot background color.
if (debug) {
printDebug("parUsr: ", c(format(digits=2, parUsr)), c("orange", "lightblue"));
}
graphics::rect(col=fill, parUsr[1], parUsr[3], parUsr[2], parUsr[4], ...);
if (!is.null(label)) {
graphics::text(mean(parUsr[c(1,2)]), mean(parUsr[c(3,4)]), label, ...);
}
}
#' Display a color raster image
#'
#' Display a color raster image
#'
#' This function augments the \code{\link[graphics]{image}} function, in
#' that it handles the useRaster parameter for non-symmetric data matrices,
#' in order to minimize the distortion from image-smoothing when pixels are
#' not square.
#'
#' The function also by default creates the image map using coordinates where
#' each integer represents the center point of one column or row of data,
#' known in the default \code{\link[graphics]{image}} function as \code{oldstyle=TRUE}.
#' For consistency, \code{imageDefault} will only accept \code{oldstyle=TRUE}.
#'
#' @param x `numeric` location of grid lines at which the intervals
#' in z are measured.
#' @param y `numeric` location of grid lines at which the intervals
#' in z are measured.
#' @param z `numeric` or logical matrix containing the values to be plotted,
#' where NA values are allowed.
#' @param zlim `numeric` range allowed for values in z.
#' @param xlim `numeric` range to plot on the x-axis, by default the x range.
#' @param ylim `numeric` range to plot on the y-axis, by default the y range.
#' @param col `character` vector of colors to be mapped to values in z.
#' @param add `logical` whether to add to an existing active R plot, or create
#' a new plot window.
#' @param xaxs `character` value compatible with graphics::par(xaxs), mainly useful
#' for suppressing the x-axis, in order to produce a custom x-axis
#' range, most useful to restrict the axis range expansion done by R
#' by default.
#' @param yaxs `character` value compatible with graphics::par(yaxs), mainly useful
#' for suppressing the y-axis, in order to produce a custom y-axis
#' range, most useful to restrict the axis range expansion done by R
#' by default.
#' @param xaxt `character` value compatible with graphics::par(xaxt), mainly useful
#' for suppressing the x-axis, in order to produce a custom x-axis
#' by other mechanisms, e.g. log-scaled x-axis tick marks.
#' @param yaxt `character` value compatible with graphics::par(yaxt), mainly useful
#' for suppressing the y-axis, in order to produce a custom y-axis
#' by other mechanisms, e.g. log-scaled y-axis tick marks.
#' @param ylab `character` label for the y-axis
#' @param xlab `character` label for the x-axis
#' @param breaks `numeric` vector of breakpoints for colors.
#' @param flip `character` string, default "none", with optional axis flip:
#' * none: perform no axis flip
#' * x: flip x-axis orientation
#' * y: flip y-axis orientation
#' * xy: flip both x- and y-axis orientation
#' @param oldstyle `logical` whether to delineate axis coordinates with an
#' integer spacing for each column and row. Note: the only allowed parameter
#' is TRUE, since useRaster=TRUE requires it. Therefore, this function
#' for consistency will only output this format.
#' @param useRaster `logical` whether to force raster image scaling, which
#' is especially useful for large data matrices. In this case a bitmap
#' raster image is created instead of polygons, then the bitmap is scaled
#' to fit the plot space. Otherwise, individual polygons can be obscured
#' on monitor screens, or may result in an extremely large file size when
#' writing to vector image format such as 'PDF' or 'SVG'.
#' @param fixRasterRatio `logical` whether to implement a simple workaround
#' to the requirement for square pixels, in the event the x- and y-axis
#' dimensions are not roughly equal.
#' @param maxRatioFix `integer` maximum number of times any axis may be
#' replicated to create a matrix of roughly equal x- and y-axis dimensions.
#' @param minRasterMultiple `integer` minimum number of times the x- and y-axis
#' will be duplicated, which is mostly useful when creating useRaster=TRUE
#' for small matrix sizes, otherwise the result will be quite blurry. For
#' example, minRasterMultiple=10 will duplicate each axis 10 times. Values
#' are aplied to rows then columns. These values are automatically defined
#' if minRasterMultiple is NULL and rasterTarget is not NULL.
#' @param rasterTarget `integer` number of cells below which cells are duplicated
#' in order to maintain detail. The default 200 defines
#' minRasterMultiple=c(1,1) if there are 200 rows and 200 columns, or
#' minRasterMultiple=c(1,100) if there are 200 rows but 2 columns.
#' @param interpolate `logical` whether to implement image interpolation,
#' by default TRUE when useRaster=TRUE.
#' @param verbose `logical` whether to enable verbose output, useful for
#' debugging.
#' @param ... Additional arguments are ignored.
#'
#' @family jam plot functions
#'
#' @returns `list` composed of elements suitable to call
#' `graphics::image.default()`.
#'
#' @seealso \code{\link[graphics]{image}}
#'
#' @examples
#' ps <- plotSmoothScatter(doTest=TRUE)
#'
#' @export
imageDefault <- function
(x=seq_len(nrow(z)+1)-0.5,
y=seq_len(ncol(z)+1)-0.5,
z,
zlim=range(z[is.finite(z)]),
xlim=range(x),
ylim=range(y),
col=grDevices::hcl.colors(12, "YlOrRd", rev=TRUE),
add=FALSE,
xaxs="i",
yaxs="i",
xaxt="n",
yaxt="n",
xlab,
ylab,
breaks,
flip=c("none", "x", "y", "xy"),
oldstyle=TRUE,
useRaster=NULL,
fixRasterRatio=TRUE,
maxRatioFix=10,
minRasterMultiple=NULL,
rasterTarget=200,
interpolate=getOption("interpolate", TRUE),
verbose=FALSE,
...)
{
## Purpose is to override image.default() by using interpolate=TRUE
## for raster images, unless otherwise specified. It also implements
## a simple technique to allow raster functions to work with substantially
## non-symmetric input data, without causing notable distortion in the
## rendered image, when fixRasterRatio=TRUE.
##
## fixRasterRatio is a simple workaround when useRaster=TRUE, which
## duplicates rows or columns to provide an approximately square matrix,
## since the useRaster methodology works best with that ratio. Otherwise
## the image manipulation assumes square pixels, and blending is stretched
## on whichever is the smaller axis dimension.
##
## minRasterMultiple=10 will duplicate the matrix 10 times for each row
## and column at a minimum, in addition to any ratio adjustment is
## performed by fixRasterRatio.
## The goal is to help with small matrices, where useRaster results in a
## blurry rasterized image. Duplicating each row and column helps to
## sharpen the edges between cells. minRasterMultiple can be a two-value
## vector, for rows and columns respectively, and is extended to length 2
## as necessary.
##
flip <- match.arg(flip);
if (interpolate && is.null(useRaster)) {
useRaster <- TRUE;
}
if (!is.null(rasterTarget)) {
rasterTarget <- rep(rasterTarget, length.out=2);
}
if (!is.null(maxRatioFix)) {
maxRatioFix <- rep(maxRatioFix, length.out=2);
}
if (missing(z)) {
if (verbose) {
printDebug("imageDefault(): ",
"missing(z)");
}
if (!missing(x)) {
if (is.list(x)) {
z <- x$z
y <- x$y
x <- x$x
} else {
if (is.null(dim(x))) {
stop("argument must be matrix-like");
}
z <- x;
x <- seq.int(0, 1, length.out=nrow(z));
# 14dec2022
y <- seq.int(0, 1, length.out=ncol(z));
}
if (missing(xlab)) {
xlab <- "";
}
if (missing(ylab)) {
ylab <- "";
}
} else {
stop("no 'z' matrix specified");
}
} else if (is.list(x)) {
if (verbose) {
jamba::printDebug("imageDefault(): ",
c("!missing(z)","is.list(x)"));
}
xn <- deparse(substitute(x))
if (missing(xlab)) {
xlab <- paste(xn, "x", sep="$");
}
if (missing(ylab)) {
ylab <- paste(xn, "y", sep="$");
}
y <- x$y;
x <- x$x;
} else {
if (verbose) {
printDebug("imageDefault(): ",
c("!missing(z)","!is.list(x)"));
}
if (missing(xlab))
if (missing(x)) {
xlab <- "";
} else {
xlab <- deparse(substitute(x));
}
if (missing(ylab)) {
if (missing(y)) {
ylab <- "";
} else {
ylab <- deparse(substitute(y));
}
}
}
if (any(!is.finite(x)) || any(!is.finite(y))) {
stop("'x' and 'y' values must be finite and non-missing");
}
if (any(diff(x) <= 0) || any(diff(y) <= 0)) {
stop("increasing 'x' and 'y' values expected");
}
if (!is.matrix(z)) {
stop("'z' must be a matrix");
}
if (length(x) > 1 && length(x) == nrow(z)) {
dx <- 0.5 * diff(x);
if (verbose) {
printDebug("imageDefault(): ",
"preparing to redefine x using diff(x)/2",
", length(x):",
length(x));
}
x <- c(
x[1] - dx[1],
x[-length(x)] + dx,
x[length(x)] + dx[length(x) - 1]
);
if (verbose) {
printDebug("imageDefault(): ",
"redefined x using diff(x)/2",
", length(x):",
length(x));
}
}
if (length(y) > 1 && length(y) == ncol(z)) {
dy <- 0.5 * diff(y);
y <- c(
y[1] - dy[1],
y[-length(y)] + dy,
y[length(y)] + dy[length(y) - 1]
);
if (verbose) {
printDebug("imageDefault(): ",
"redefined y using diff(y)/2",
", length(y):",
length(y));
}
} else {
if (verbose) {
printDebug("imageDefault(): ",
"did not redefine y using diff(y)/2",
", length(y):",
length(y));
}
}
if (missing(breaks)) {
nc <- length(col);
if (!missing(zlim) && (any(!is.finite(zlim)) || diff(zlim) < 0)) {
stop("invalid z limits");
}
if (diff(zlim) == 0) {
if (zlim[1] == 0) {
zlim <- c(-1, 1);
} else {
zlim <- zlim[1] + c(-0.4, 0.4) * abs(zlim[1]);
}
}
z <- (z - zlim[1])/diff(zlim);
if (oldstyle) {
zi <- floor((nc - 1) * z + 0.5);
} else {
zi <- floor((nc - 1e-05) * z + 1e-07);
}
zi[zi < 0 | zi >= nc] <- NA;
} else {
if (length(breaks) != length(col) + 1) {
stop("must have one more break than colour");
}
if (any(!is.finite(breaks))) {
stop("breaks must all be finite");
}
# remove check for R version < 3
zi <- .bincode(x=z,
breaks=breaks,
right=TRUE,
include.lowest=TRUE) - 1L;
}
if (igrepHas("y", flip)) {
ylim <- rev(ylim);
}
if (igrepHas("x", flip)) {
xlim <- rev(xlim);
}
if (verbose) {
printDebug("imageDefault(): ",
"xlim:",
xlim);
printDebug("imageDefault(): ",
"ylim:",
ylim);
}
if (!add) {
plot(NA,
NA,
xlim=xlim,
ylim=ylim,
type="n",
xaxs=xaxs,
yaxs=yaxs,
xaxt=xaxt,
yaxt=yaxt,
xlab=xlab,
ylab=ylab,
...);
}
if (length(x) <= 1) {
x <- graphics::par("usr")[1:2];
}
if (length(y) <= 1) {
y <- graphics::par("usr")[3:4];
}
if (length(x) != nrow(z) + 1 || length(y) != ncol(z) + 1) {
stop("dimensions of z are not length(x)(-1) times length(y)(-1)");
}
if (length(minRasterMultiple) == 0) {
if (!is.null(rasterTarget)) {
minRasterMultiple <- c(ceiling(rasterTarget[1]/ncol(z)),
ceiling(rasterTarget[2]/nrow(z)));
} else {
minRasterMultiple <- c(1, 1);
}
} else {
minRasterMultiple <- rep(minRasterMultiple, length.out=2);
}
if (verbose) {
printDebug("minRasterMultiple:", minRasterMultiple);
}
check_irregular <- function(x, y) {
dx <- diff(x);
dy <- diff(y);
(
length(dx) &&
!isTRUE(all.equal(dx, rep(dx[1], length(dx))))
) || (
length(dy) &&
!isTRUE(all.equal(dy, rep(dy[1], length(dy))))
);
}
if (is.null(useRaster)) {
useRaster <- getOption("preferRaster", FALSE);
if (useRaster) {
useRaster <- FALSE;
ras <- grDevices::dev.capabilities("raster");
if (identical(ras, "yes")) {
useRaster <- TRUE;
}
if (identical(ras, "non-missing")) {
useRaster <- all(!is.na(zi));
}
}
}
if (verbose) {
printDebug("imageDefault(): ",
"useRaster: ",
useRaster);
}
if (useRaster && fixRasterRatio > 1) {
} else if (useRaster && fixRasterRatio) {
## To try to deal with the raster function not handling the case of
## non-square data matrices, we'll try to make the data roughly square by
## duplicating some data
##
## First we'll only handle when there is more than 2:1 ratio. Everything
## else is close enough not to bother
if (verbose) {
printDebug("imageDefault(): ",
"dim(z): ", dim(z));
printDebug("imageDefault(): ",
"dim(zi): ", dim(zi));
printDebug("imageDefault(): ",
"length(x): ", length(x));
printDebug("imageDefault(): ",
"length(y): ", length(y));
printDebug("imageDefault(): ",
"head(x): ", head(round(digits=2,x),20));
printDebug("imageDefault(): ",
"tail(x): ", tail(round(digits=2,x),20));
printDebug("imageDefault(): ",
"head(y): ", head(round(digits=2,y),20));
printDebug("imageDefault(): ",
"tail(y): ", tail(round(digits=2,y),20));
}
if (any(minRasterMultiple > 1) ||
(length(y)-1) > 2*(length(x)-1) ||
(length(x) > 2*length(y)) ) {
if (verbose) {
printDebug("imageDefault(): ",
c("Fixing the raster ratio."));
}
dimRange <- range(c(length(x), length(y)));
if (length(x) > 2*length(y)) {
dupRowX <- floor((length(x)-1)/(length(y)-1));
dupRowX <- min(c(dupRowX, maxRatioFix[1]));
dupColX <- 1;
} else {
dupColX <- floor((length(y)-1)/(length(x)-1));
dupColX <- min(c(dupColX, maxRatioFix[2]));
dupRowX <- 1;
}
if (verbose) {
printDebug("imageDefault(): ",
"dupColX: ",
dupColX);
printDebug("imageDefault(): ",
"dupRowX: ",
dupRowX);
}
## Ensure that minRasterMultiple is applied
dupColX <- min(c(maxRatioFix[2],
max(c(dupColX, minRasterMultiple[2]))));
dupRowX <- min(c(maxRatioFix[1],
max(c(dupRowX, minRasterMultiple[1]))));
if (verbose) {
printDebug("imageDefault(): ",
"maxRatioFix:",
maxRatioFix);
printDebug("imageDefault(): ",
"dupColX: ",
dupColX);
printDebug("imageDefault(): ",
"dupRowX: ",
dupRowX);
}
newCols <- rep(1:(length(x)-1), each=dupColX);
newRows <- rep(1:(length(y)-1), each=dupRowX);
## Column processing
xNcolSeq <- seq(from=0.5,
to=(length(x)-1)+0.5,
length.out=length(newCols)+1);
## 29oct2018 modify so the x range is same as before
xNcolSeq <- normScale(xNcolSeq,
from=min(x),
to=max(x));
newColBreaks <- breaksByVector(newCols);
newColLabels <- newColBreaks$newLabels;
## Row processing
yNrowSeq <- seq(from=0.5, to=(length(y)-1)+0.5,
length.out=length(newRows)+1);
## 29oct2018 modify so the x range is same as before
yNrowSeq <- normScale(yNrowSeq,
from=min(y),
to=max(y));
newRowBreaks <- breaksByVector(newRows);
newRowLabels <- newRowBreaks$newLabels;
dim(zi) <- dim(z);
z <- z[newCols,,drop=FALSE];
zi <- zi[newCols,,drop=FALSE];
z <- z[,newRows,drop=FALSE];
zi <- zi[,newRows,drop=FALSE];
x <- xNcolSeq;
y <- yNrowSeq;
} else if (length(x) > 2*length(y)) {
dupRowX <- floor((length(x)-1)/(length(y)-1));
newRows <- rep(1:(length(y)-1), each=dupRowX);
yNrowSeq <- seq(from=0.5, to=(length(y)-1)+0.5,
length.out=length(newRows)+1);
## 14dec2022 modify so the y range is same as before
# yNrowSeq <- normScale(yNrowSeq,
# from=min(y),
# to=max(y));
newRowBreaks <- breaksByVector(newRows);
newRowLabels <- newRowBreaks$newLabels;
dim(zi) <- dim(z);
z <- z[,newRows,drop=FALSE];
zi <- zi[,newRows,drop=FALSE];
y <- yNrowSeq;
}
}
if (useRaster) {
if (check_irregular(x, y)) {
stop("useRaster=TRUE can only be used with a regular grid");
}
if (!is.character(col)) {
p <- grDevices::palette();
pl <- length(p);
col <- as.integer(col);
col[col < 1L] <- NA_integer_;
col <- p[((col - 1L)%%pl) + 1L];
}
zc <- col[zi + 1L];
dim(zc) <- dim(z);
zc <- t(zc)[ncol(zc):1L, , drop=FALSE];
graphics::rasterImage(
image=grDevices::as.raster(zc),
xleft=min(x),
ybottom=min(y),
xright=max(x),
ytop=max(y),
interpolate=interpolate);
return(invisible(list(
x=x,
y=y,
zi=zi,
col=col,
zc=zc)))
} else {
# call image.default() and let it render non-rasterized output
graphics::image.default(x=x,
y=y,
z=zi,
col=col,
add=TRUE,
breaks=breaks,
oldstyle=oldstyle,
useRaster=FALSE,
...)
return(invisible(list(
x=x,
y=y,
zi=zi,
col=col,
zc=NULL)))
}
}
#' Draw text with shadow border
#'
#' Draw text with shadow border
#'
#' Draws text with the same syntax as \code{graphics::text()} except that
#' this function adds a contrasting color border around the text, which
#' helps visibility when the background color is either not known, or is
#' not expected to be a fixed contrasting color.
#'
#' The function draws the label n times with the chosed background
#' color, then the label itself atop the background text. It does not
#' typically have a noticeable effect on rendering time, but it may
#' impact downstream uses in vector file formats like 'SVG' and 'PDF', where
#' text is stored as proper text and font objects. Take care when editing
#' text that the underlying shadow text is also edited in sync.
#'
#' The parameter \code{doTest=TRUE} will display a visual example. The
#' background color can be modified with \code{fill="navy"} for example.
#'
#' @family jam plot functions
#'
#' @param x,y numeric coordinates, either as vectors x and y, or x as a
#' two-color matrix recognized by `grDevices::xy.coords()`.
#' @param labels vector of labels to display at the corresponding xy
#' coordinates.
#' @param col,bg,shadowColor the label color, and background (outline) color,
#' and shadow color (if `shadow=TRUE`), for each
#' element in `labels`. Colors are applied in order, and recycled to
#' `length(labels)` as needed. By default `bg` will choose
#' a contrasting color, based upon `setTextContrastColor()`.
#' Also by default, the shadow is "black" true to its name, since it is
#' expected to darken the area around it.
#' @param r the outline radius, expressed as a fraction of the width of the
#' character "A" as returned by `graphics::strwidth()`.
#' @param offset the outline offset position in xy coordinates, expressed
#' as a fraction of the width of the character "A" as returned by
#' `graphics::strwidth()`, and `graphics::strheight()`,
#' respectively.
#' The offset is only applied when `shadow=TRUE` to enable the shadow
#' effect.
#' @param n `numeric` steps around the label used to create the outline.
#' A higher number may be useful for very large font sizes, otherwise 8
#' is a reasonably good balance between detail and the number of labels
#' added.
#' @param outline `logical` whether to enable outline drawing.
#' @param shadow `logical` whether to enable shadow drawing.
#' @param alphaOutline,alphaShadow `numeric` alpha transparency to use
#' for the outline and shadow colors, respectively.
#' @param doTest `logical` whether to create a visual example of output. Note
#' that it calls \code{\link{usrBox}} to color the plot area, and the
#' background can be overridden with something like \code{fill="navy"}.
#' @param shadowOrder `character` value indicating when shadows are drawn
#' relative to drawing labels: `"each"` draws each shadow with each label,
#' so that shadows will overlap previous labels; `"all"` draws all shadows
#' first then all labels, so labels will always appear above all
#' shadows. See examples.
#' @param cex `numeric` scalar applied to font size, default `graphics::par("cex")`.
#' @param font `character` applied to font family, default `graphics::par("font")`.
#' @param ... other parameters are passed to \code{\link[graphics]{text}}.
#' Note that certain parameters are not vectorized in that function,
#' such as \code{srt} which requires only a fixed value. To rotate each
#' label independently, multiple calls to \code{\link[graphics]{text}} or
#' \code{\link{shadowText}} must be made. Other parameters like \code{adj}
#' only accept up to two values, and those two values affect all label
#' positioning.
#'
#' @returns invisible `list` of components used to call `graphics::text()`,
#' including: x, y, allColors, allLabels, cex, font.
#'
#' @examples
#' shadowText(doTest=TRUE);
#' shadowText(doTest=TRUE, fill="navy");
#' shadowText(doTest=TRUE, fill="red4");
#'
#' # example showing labels with overlapping shadows
#' withr::with_par(list("mfrow"=c(1, 2)), {
#' nullPlot(doBoxes=FALSE);
#' graphics::title(main="shadowOrder='each'");
#' shadowText(x=c(1.5, 1.65), y=c(1.5, 1.55),
#' labels=c("one", "two"), cex=c(2, 4), shadowOrder="each")
#' nullPlot(doBoxes=FALSE);
#' graphics::title(main="shadowOrder='all'");
#' shadowText(x=c(1.5, 1.65), y=c(1.5, 1.55),
#' labels=c("one", "two"), cex=c(2, 4), shadowOrder="all")
#' })
#'
#' @export
shadowText <- function
(x,
y=NULL,
labels=NULL,
col="white",
bg=setTextContrastColor(col),
r=getOption("jam.shadow.r", 0.15),
offset=c(0.15, -0.15),
n=getOption("jam.shadow.n", 8),
outline=getOption("jam.outline", TRUE),
alphaOutline=getOption("jam.alphaOutline", 0.4),
shadow=getOption("jam.shadow", FALSE),
shadowColor=getOption("jam.shadowColor", "black"),
alphaShadow=getOption("jam.alphaShadow", 0.2),
shadowOrder=c("each", "all"),
cex=graphics::par("cex"),
font=graphics::par("font"),
doTest=FALSE,
...)
{
## Purpose is to draw text with a border around it to help
## make text more visible even with light and dark features
## beneath the text.
## It can be used by overriding the text function prior to
## running plot functions, e.g.
## 'text<-shadowText'.
## Then reset to the default text function afterwards, e.g.
## 'text<-graphics::text'
##
## doTest=TRUE will display an example
#cex <- rep(cex, length.out=length(labels));
#font <- rep(font, length.out=length(labels));
#srt <- rep(srt, length.out=length(labels));
shadowOrder <- match.arg(shadowOrder);
if (length(alphaOutline) == 0) {
alphaOutline <- 0.7;
options("jam.alphaOutline"=0.7);
}
if (doTest) {
## Example shadow text
nullPlot(xlim=c(1,9),
ylim=c(0,10),
doBoxes=FALSE,
doUsrBox=TRUE,
...);
if (length(col) == 1 && col == "white") {
col <- c("white", "white", "yellow", "green4", "red4", "blue");
bg <- setTextContrastColor(col);
}
if (length(labels) == 0) {
labels <- LETTERS[1:12];
} else {
labels <- rep(labels, length.out=12);
}
st1 <- shadowText(x=rep(2,9), y=9:1,
labels=c("outline=FALSE","shadow=FALSE",labels[1:7]),
outline=FALSE, shadow=FALSE,
col=col,
bg=bg,
cex=c(1.1, 1, 1, 1, 1, 1, 1, 1, 1),
offset=offset, n=n, r=r,
doTest=FALSE);
st2 <- shadowText(x=rep(4,9), y=9:1-0.3,
labels=c("outline=TRUE","shadow=FALSE",labels[1:7]),
outline=TRUE, shadow=FALSE,
col=col,
bg=bg,
cex=c(1.1, 1, 1, 1, 1, 1, 1, 1, 1),
offset=offset, n=n, r=r,
doTest=FALSE);
st3 <- shadowText(x=rep(6,9), y=9:1,
labels=c("outline=FALSE","shadow=TRUE",labels[1:7]),
outline=FALSE, shadow=TRUE,
col=col,
bg=bg,
cex=c(1, 1.1, 1, 1, 1, 1, 1, 1, 1),
offset=offset, n=n, r=r,
doTest=FALSE);
st4 <- shadowText(x=rep(8,9), y=9:1-0.3,
labels=c("outline=TRUE","shadow=TRUE",labels[1:7]),
outline=TRUE, shadow=TRUE,
col=col,
bg=bg,
cex=c(1.1, 1.1, 1, 1, 1, 1, 1, 1, 1),
offset=offset, n=n, r=r,
doTest=FALSE);
return(invisible(list(st1=st1, st2=st2, st3=st3)));
}
cex <- rep(cex, length.out=length(labels));
font <- rep(font, length.out=length(labels));
bg <- rep(bg, length.out=length(labels));
xy <- grDevices::xy.coords(x, y);
xo <- r * graphics::strwidth("A");
yo <- r * graphics::strheight("A");
if (length(offset) == 0) {
offset <- c(0.15, 0.15);
}
offset <- rep(offset, length.out=2);
offsetX <- offset[1] * graphics::strwidth("A");
offsetY <- offset[2] * graphics::strheight("A");
## Angular sequence with n steps
theta <- tail(seq(0, 2*pi, length.out=n+1), -1);
## Outline has no offset
if (outline) {
## Make a matrix of coordinates per label
outlineX <- matrix(ncol=n,
byrow=TRUE,
rep(xy$x, each=n) + cos(theta) * xo);
outlineY <- matrix(ncol=n,
byrow=TRUE,
rep(xy$y, each=n) + sin(theta) * yo);
outlineLabels <- matrix(ncol=n,
byrow=TRUE,
rep(labels, each=n));
outlineColors <- matrix(ncol=n,
byrow=TRUE,
nrow=length(labels),
rep(alpha2col(bg, alpha=alphaOutline), each=n));
} else {
outlineX <- outlineY <- outlineLabels <- outlineColors <- NULL;
}
## Shadow has offset
if (shadow) {
## Make a matrix of coordinates per label
shadowX <- matrix(ncol=n, byrow=TRUE,
rep(xy$x + offsetX, each=n) + cos(theta)*xo*1.5);
shadowY <- matrix(ncol=n, byrow=TRUE,
rep(xy$y + offsetY, each=n) + sin(theta)*yo*1.5);
shadowLabels <- matrix(ncol=n, byrow=TRUE,
rep(labels, each=n));
shadowColors <- matrix(ncol=n, nrow=length(labels), byrow=TRUE,
rep(alpha2col(shadowColor, alpha=alphaShadow), each=n));
} else {
shadowX <- shadowY <- shadowLabels <- shadowColors <- NULL;
}
## Append label coordinates to shadow coordinates so the shadows
## are drawn first, for each label in order. This order ensures
## that overlaps are respected without any labels appearing above
## another label shadow out of order.
allX <- cbind(shadowX, outlineX, xy$x);
allY <- cbind(shadowY, outlineY, xy$y);
allColors <- cbind(shadowColors, outlineColors,
rep(col, length.out=length(labels)));
allLabels <- cbind(shadowLabels, outlineLabels, labels);
if ("each" %in% shadowOrder) {
allX <- t(allX);
allY <- t(allY);
allColors <- t(allColors);
allLabels <- t(allLabels);
cex <- rep(cex, each=nrow(allX));
font <- rep(font, each=nrow(allX));
}
#allCex <- rep(cex, n+1);
#allFont <- rep(font, n+1);
#allSrt <- rep(srt, n+1);
## Draw labels with one graphics::text() call to make it vectorized
graphics::text(x=c(allX),
y=c(allY),
labels=c(allLabels),
col=c(allColors),
cex=cex,
font=font,
...);
return(invisible(list(
allX=allX,
allY=allY,
allColors=allColors,
allLabels=allLabels,
cex=cex,
font=font)));
}
#' Adjust axis label margins
#'
#' Adjust axis label margins to accommodate axis labels
#'
#' This function takes a vector of axis labels, and the margin where they
#' will be used, and adjusts the relevant axis margin to accomodate the
#' label size, up to a maximum fraction of the figure size as defined by
#' `maxFig`.
#'
#' Labels are assumed to be perpendicular to the axis, for example
#' argument `las=2` when using `graphics::text()`.
#'
#' Note this function does not render labels in the figure, and therefore
#' does not revert axis margins to their original size. That process
#' should be performed separately.
#'
#' @family jam plot functions
#'
#' @param x `character` vector of axis labels
#' @param margin `integer` value indicating which margin to adjust,
#' using the order by \code{graphics::par("mar")}, 1=bottom, 2=left, 3=top,
#' 4=right.
#' @param maxFig `numeric` fraction less than 1, indicating the maximum
#' size of margin relative to the figure size. Setting margins too
#' large results in an error otherwise.
#' @param cex `numeric` or NULL, default `graphics::par("cex")`,
#' used as a convenience with `cex * cex.axis` passed to
#' `graphics::strwidth()`.
#' However, `graphics::axis()` itself should use `cex.axis` when
#' adjusting axis label font size.
#' @param cex.axis `numeric`, default `graphics::par("cex.axis")` to define
#' the axis label font size.
#' @param prefix `character` string to add whitespace around the axis label
#' in order to add a "buffer" of whitespace.
#' @param ... additional parameters are ignored.
#'
#' @returns `list` named "mai" suitable for use in `graphics::par()`
#' to adjust margin size using in inches.
#'
#' @examples
#' xlabs <- paste0("item_", (1:20));
#' ylabs <- paste0("rownum_", (1:20));
#'
#' # proper adjustment should be done using withr, for example
#' x_cex <- 0.8;
#' y_cex <- 1.2;
#' withr::with_par(adjustAxisLabelMargins(xlabs, 1, cex.axis=x_cex), {
#' withr::local_par(adjustAxisLabelMargins(ylabs, 2, cex.axis=y_cex))
#' nullPlot(xlim=c(1,20), ylim=c(1,20), doMargins=FALSE);
#' graphics::axis(1, at=1:20, labels=xlabs, las=2, cex.axis=x_cex);
#' graphics::axis(2, at=1:20, labels=ylabs, las=2, cex.axis=y_cex);
#' })
#'
#' withr::with_par(adjustAxisLabelMargins(xlabs, 3, cex.axis=x_cex), {
#' withr::local_par(adjustAxisLabelMargins(ylabs, 4, cex.axis=y_cex))
#' nullPlot(xlim=c(1,20), ylim=c(1,20), doMargins=FALSE);
#' graphics::axis(3, at=1:20, labels=xlabs, las=2);
#' graphics::axis(4, at=1:20, labels=ylabs, las=2);
#' })
#'
#' par("mar")
#' @export
adjustAxisLabelMargins <- function
(x,
margin=1,
maxFig=1/2,
cex=graphics::par("cex"),
cex.axis=graphics::par("cex.axis"),
prefix="-- -- ",
...)
{
## Purpose is to adjust figure margins to accomodate label string length
## but no greater than the maxFig proportion of figure size.
##
## x is a vector of axis labels
##
## graphics::par("mai") and graphics::par("fin") are used, with units="inches", which allows
## the calculations to remain unaware of plot coordinates.
##
## The margin values refer to the order from graphics::par("mar"),
## 1-bottom, 2-left, 3-top, 4-right
## Note: If a plot device is not already open, the call to graphics::strwidth()
## will open one if possible. If not possible, an error will be thrown
## from graphics::strwidth().
if (!margin %in% c(1,2,3,4)) {
stop("adjustAxisLabelMargins() requires margin to be one of c(1,2,3,4).");
}
## Get plot and figure sizes in inches
parMai <- graphics::par("mai");
parFin <- graphics::par("fin");
cex_use <- cex * cex.axis;
maxWidth <- max(
graphics::strwidth(paste(prefix, x),
units="inches",
cex=cex_use) + 0.2,
na.rm=TRUE);
## Make sure label margins are not more than 1/2 the figure size
refMargin <- 2-(margin %% 2);
parMaiNew <- min(c(maxWidth, parFin[refMargin]*maxFig));
parMai[margin] <- parMaiNew;
invisible(list(mai=parMai));
}
#' Plot distribution and histogram overlay
#'
#' Plot distribution and histogram overlay
#'
#' This function is a wrapper around `graphics::hist()` and
#' `stats::density()`, with enough customization to cover
#' most of the situations that need customization.
#'
#' For example `log="x"` will automatically log-transform the x-axis,
#' keeping the histogram bars uniformly sized. Alternatively,
#' `xScale="sqrt"` will square root transform the data, and
#' transform the x-axis while keeping the numeric values constant.
#'
#' It also scales the density profile height to be similar to
#' the histogram bar height, using the 99th quantile of the y-axis
#' value, which helps prevent outlier peaks from dominating the
#' y-axis range, thus obscuring interesting smaller features.
#'
#' If supplied with a data matrix, this function will create a layout
#' with `ncol(x)` panels, and plot the distribution of each column
#' in its own panel, using categorical colors from `rainbow2()`.
#'
#' For a similar style using ggplot2, see `plotRidges()`, which displays
#' only the density profile for each sample, but in a much more scalable
#' format for larger numbers of columns.
#'
#' By default NA values are ignored, and the distributions represent
#' non-NA values.
#'
#' Colors can be controlled using the parameter `col`, but can
#' be specifically defined for bars with `barCol` and the polygon
#' with `polyCol`.
#'
#' @family jam plot functions
#'
#' @returns invisible `list` with density and histogram data output,
#' however this function is called for the by-product of its plot
#' output.
#'
#' @param x `numeric` vector, or `numeric` matrix. When a matrix is
#' provided, each column in the matrix is used as its own data source.
#' @param doHistogram `logical` indicating whether to plot histogram bars.
#' @param doPolygon `logical` indicating whether to plot the density polygon.
#' @param col `character` color, or when `x` is supplied as a matrix,
#' a vector of colors is applied to across plot panels.
#' Note that `col` will override all colors defined for `barCol`, `polyCol`,
#' `histBorder`, `polyBorder`.
#' @param barCol,polyCol,polyBorder,histBorder `character` colors used
#' when `col` is not supplied.
#' They define colors for the histogram bars, polygon fill,
#' polygon border, and histogram bar border, respectively.
#' @param colAlphas `numeric` vector with length 3, indicating the alpha
#' transparency to use for histogram bar fill, polygon density fill,
#' and border color, respectively.
#' Alpha transparency should be scaled between 0 (fully transparent)
#' and 1 (fully opaque).
#' These alpha transparency values are applied to each color in `col`
#' when `col` is defined.
#' @param darkFactors `numeric` used to adjust colors when `col` is defined.
#' Values are applied to histogram bar fill, polygon density fill,
#' and border color, respectively, by calling `makeColorDarker()`.
#' @param lwd `numeric` line width.
#' @param las `integer` used to define axis label orientation.
#' @param u5.bias,pretty.n `numeric` arguments passed to to `base::pretty()`
#' to define pretty axis label positions.
#' @param bw `character` string of the bandwidth name to use in the
#' density calculation, passed to `jamba::breakDensity()`.
#' By default `stats::density()` calls a very smooth density kernel,
#' which obscures finer details, so the default in
#' `jamba::breakDensity()` uses a more detailed kernel.
#' @param breaks `numeric` breaks sent to `hist` to define the number of
#' histogram bars. It can be in the form of a single `integer` number
#' of equidistant breaks, or a `numeric` vector with specific break
#' positions, but remember to include a starting value lower the the
#' lowest value in `x`, and an ending value higher than the highest
#' value in `x`. Passed to `breakDensity()`.
#' @param width `numeric` passed to `breakDensity()`.
#' @param densityBreaksFactor `numeric` scaling factor to control
#' the level of detail in the density, passed to `breakDensity()`.
#' @param axisFunc `function` optionally used in place of `graphics::axis()` to define
#' axis labels.
#' @param bty `character` string used to define the plot box shape,
#' see `graphics::box()`.
#' @param cex.axis `numeric` scalar to adjust axis label font size.
#' @param doPar `logical` indicating whether to apply `graphics::par()`, specifically
#' when `x` is supplied as a multi-column matrix. When `doPar=FALSE`,
#' no panels nor margin adjustments are made at all.
#' @param heightFactor `numeric` value indicating the height of the y-axis
#' plot scale to use when scaling the histogram and polygon density
#' within each plot panel.
#' @param weightFactor `numeric` passed to `breakDensity()`.
#' @param main `character` title to display above the plot, used only when
#' `x` is supplied as a single `numeric` vector. Otherwise each plot
#' title uses the relevant `colnames(x)` value.
#' @param xaxs,yaxs,xaxt,yaxt `character` string indicating the type of
#' x-axis and y-axis to render, see `graphics::par()`.
#' @param xlab,ylab `character` labels for x-axis and y-axis, respectively.
#' @param log `character` vector, optionally containing `"x"` and/or `"y"` to
#' to indicate which axes are log-transformed. If `"x" %in% log`
#' then it sets `xScale="log10"`, both methods are equivalent in
#' defining the log-transformation of the x-axis.
#' @param xScale `character` string to define the x-axis transformation:
#' * `"default"` applies no transform;
#' * `"log10"` applies a log10 transform, specifically `log10(x + 1)`
#' * `"sqrt"` applies a sqrt transform.
#' @param usePanels `logical` indicating whether to separate
#' the density plots into panels when `x` contains multiple columns.
#' When `useOnePanel=FALSE` the panels will be defined so that all
#' columns will fit on one page.
#' @param useOnePanel `logical` indicating whether to define multiple panels
#' on one page. Therefore `useOnePanel=TRUE` will create multiple
#' pages with one panel on each page, which may work well for
#' output in multi-page 'PDF' files.
#' @param ablineV,ablineH `numeric` vector representing abline
#' vertical and horizontal positions, respectively.
#' These values are mostly helpful in multi-panel plots,
#' to draw consistent reference lines on each panel.
#' @param ablineVcol,ablineHcol, default"#44444499", with the abline
#' color, used when `ablineV` or `ablineH` are supplied, respectively.
#' @param ablineVlty,ablineHlty `numeric` or `character` indicating the
#' line type to use for `ablineV` and `ablineH`, respectively.
#' @param removeNA `logical` indicating whether to remove NA values
#' prior to running histogram and density calculations. Presence
#' of NA values generally causes both functions to fail.
#' @param add `logical` indicating whether to add the plot to an existing
#' visualization.
#' @param ylimQuantile `numeric` value between 0 and 1, indicating the
#' quantile value of the density `y` values to use for the ylim. This
#' threshold is only applied when `ylim` is NULL.
#' @param ylim,xlim `numeric` y-axis and x-axis ranges, respectively.
#' When either is `NULL`, the axis range is determined independently
#' for each plot panel. Either value can be supplied as a `list`
#' to control the numeric range for each individual plot, relevant
#' only when `x` is supplied as a multi-column matrix.
#' @param highlightPoints `character` vector of optional rownames,
#' or `integer` values with row indices, for rows to be highlighted.
#' When `x` is supplied as a `matrix`, `highlightPoints` can
#' be supplied as a `list` of vectors, referring to each column in `x`.
#' When rows are highlighted, the plot is drawn with all points,
#' then the highlighted points are drawn again over the histogram bars,
#' and polygon density, as relevant.
#' @param highlightCol `character` vector of colors to
#' use to fill the histogram when `highlightPoints` is supplied.
#' Multiple values are recycled one per column in `x`,
#' if `x` is supplied as a multi-column matrix.
#' @param verbose `logical` indicating whether to print verbose output.
#' @param ... additional arguments are passed to relevant internal
#' functions.
#'
#' @examples
#' # basic density plot
#' set.seed(123);
#' x <- stats::rnorm(2000);
#' plotPolygonDensity(x, main="basic polygon density plot");
#'
#' # fewer breaks
#' plotPolygonDensity(x,
#' breaks=20,
#' main="breaks=20");
#'
#' # log-scaled x-axis
#' plotPolygonDensity(10^(3+stats::rnorm(2000)), log="x",
#' breaks=50,
#' main="log-scaled x-axis");
#'
#' # highlighted points
#' set.seed(123);
#' plotPolygonDensity(x,
#' highlightPoints=sample(which(abs(x) > 1), size=200),
#' breaks=40,
#' main="breaks=40");
#'
#' # hide axis labels
#' set.seed(123);
#' plotPolygonDensity(x,
#' highlightPoints=sample(which(abs(x) > 1), size=200),
#' breaks=40,
#' xaxt="n",
#' yaxt="n",
#' main="breaks=40");
#'
#' # multiple columns
#' set.seed(123);
#' xm <- do.call(cbind, lapply(1:4, function(i){stats::rnorm(2000)}))
#' plotPolygonDensity(xm, breaks=20)
#'
#' @export
plotPolygonDensity <- function
(x,
doHistogram=TRUE,
doPolygon=TRUE,
col=NULL,
barCol="#00337799",
polyCol="#00449977",
polyBorder=makeColorDarker(polyCol),
histBorder=makeColorDarker(barCol, darkFactor=1.5),
colAlphas=c(0.8,0.6,0.9),
darkFactors=c(-1.3, 1, 3),
lwd=2,
las=2,
u5.bias=0,
pretty.n=10,
bw=NULL,
breaks=100,
width=NULL,
densityBreaksFactor=3,
axisFunc=graphics::axis,
bty="l",
cex.axis=1.5,
doPar=TRUE,
heightFactor=0.95,
weightFactor=NULL,#weightFactor=0.22*(100/breaks),
main="Histogram distribution",
xaxs="i",
yaxs="i",
xaxt="s",
yaxt="s",
xlab="",
ylab="",
log=NULL,
xScale=c("default","log10","sqrt"),
usePanels=TRUE,
useOnePanel=FALSE,
ablineV=NULL,
ablineH=NULL,
ablineVcol="#44444499",
ablineHcol="#44444499",
ablineVlty="solid",
ablineHlty="solid",
removeNA=TRUE,
add=FALSE,
ylimQuantile=0.99,
ylim=NULL,
xlim=NULL,
highlightPoints=NULL,
highlightCol="gold",
verbose=FALSE,
...)
{
## Purpose is to wrapper a plot(stats::density(x)) and graphics::polygon() method
## for pretty filled density plots
##
## log="x" will impose log10 scale to the x-axis, and display log-axis tick marks
##
## xScale is an extension to log="x" which allows setting the scale
## to other transforms, e.g. sqrt.
##
## TODO: fix the density-to-histogram visual scaling
## "Rescale density to histogram1"
##
## ylimQuantile is used to scale the y-axis such that one giant bar
## does not shrink the remaining visible y-axis scale so much that detail
## is lost. To show the full y-axis range, use ylimQuantile=1.
## This value is only applied when ylim=NULL.
##
xScale <- match.arg(xScale);
## ablineV will include graphics::abline(s) in each panel
if (!is.null(ablineV)) {
ablineVcol <- rep(ablineVcol, length.out=length(ablineV));
ablineVlty <- rep(ablineVlty, length.out=length(ablineV));
}
if (!is.null(ablineH)) {
ablineHcol <- rep(ablineHcol, length.out=length(ablineH));
ablineHlty <- rep(ablineHlty, length.out=length(ablineH));
}
## Optionally, if the input data is a multi-color matrix, split into separate panels
if (igrepHas("matrix|data.*frame|tibble|data.table", class(x)) &&
ncol(x) > 1 &&
TRUE %in% usePanels) {
## ablineV will include graphics::abline(s) in each panel
if (!is.null(ablineV)) {
ablineV <- rep(ablineV, length.out=ncol(x));
}
newMfrow <- decideMfrow(ncol(x));
if (useOnePanel) {
newMfrow <- c(1, 1);
}
if (doPar) {
withr::local_par("mfrow"=newMfrow);
}
if (length(barCol) == ncol(x)) {
panelColors <- barCol;
} else {
# if (check_pkg_installed("colorjam")) {
# panelColors <- colorjam::rainbowJam(ncol(x));
# } else {
panelColors <- rainbow2(ncol(x));
# panelColors <- sample(unvigrep("gr[ae]y|white|black|[34]$",
# grDevices::colors()),
# size=ncol(x));
# }
}
if (length(colnames(x)) == 0) {
colnames(x) <- makeNames(rep("column", ncol(x)), suffix="_");
}
## Handle highlightPoints
if (length(highlightPoints) > 0) {
if (!is.list(highlightPoints)) {
highlightPoints <- list(highlightPoints);
}
if (length(highlightPoints) < ncol(x)) {
highlightPoints <- rep(highlightPoints, length.out=ncol(x));
}
if (length(highlightCol) == 0) {
highlightCol <- "gold";
}
highlightCol <- rep(highlightCol, length.out=ncol(x));
}
## Get common set of breaks
#hx <- graphics::hist(x, breaks=breaks, plot=FALSE, ...);
#breaks <- hx$breaks;
## Iterate each column
# recycle xlim if present
if (length(xlim) > 0) {
if (!is.list(xlim)) {
xlim <- list(range(xlim, na.rm=TRUE));
}
xlim <- rep(xlim, length.out=ncol(x));
} else {
xlim <- NULL
}
# recycle ylim if present
if (length(ylim) > 0) {
if (!is.list(ylim)) {
ylim <- list(range(ylim, na.rm=TRUE));
}
ylim <- rep(ylim, length.out=ncol(x));
} else {
ylim <- NULL
}
d1 <- lapply(nameVector(seq_len(ncol(x)), colnames(x)), function(i){
if (useOnePanel) {
add <- (i > 1);
mainTitle <- "";
} else {
mainTitle <- colnames(x)[i];
}
xi <- x[,i];
if (length(rownames(x)) > 0) {
names(xi) <- rownames(x);
}
if (removeNA) {
xi <- rmNA(xi);
}
d2 <- plotPolygonDensity(xi,
main=mainTitle,
barCol=alpha2col(panelColors[i], alpha=colAlphas[1]),
polyCol=alpha2col(panelColors[i], alpha=colAlphas[2]),
doPolygon=doPolygon,
polyBorder=alpha2col(panelColors[i], alpha=colAlphas[3]),
doHistogram=doHistogram,
add=add,
colAlphas=colAlphas,
doPar=FALSE,
col=col,
lwd=lwd,
las=las,
u5.bias=u5.bias,
pretty.n=pretty.n,
bw=bw,
breaks=breaks,
width=width,
axisFunc=axisFunc,
bty=bty,
cex.axis=cex.axis,
heightFactor=heightFactor,
weightFactor=weightFactor,
xaxs=xaxs,
yaxs=yaxs,
xaxt=xaxt,
yaxt=yaxt,
log=log,
xScale=xScale,
verbose=verbose,
ablineV=ablineV,
ablineVcol=ablineVcol,
ablineVlty=ablineVlty,
ablineH=ablineH,
ablineHcol=ablineHcol,
ablineHlty=ablineHlty,
ylimQuantile=ylimQuantile,
ylim=ylim[[i]],
xlim=xlim[[i]],
highlightPoints=highlightPoints[[i]],
highlightCol=highlightCol[[i]],
...);
d2;
});
if (useOnePanel && ncol(x) > 1) {
graphics::legend("top",
inset=c(0,0.05),
legend=colnames(x),
fill=alpha2col(panelColors, alpha=colAlphas[2]),
border=alpha2col(panelColors, alpha=colAlphas[3]));
}
invisible(d1);
} else {
##
withr::local_par(list("xaxs"=xaxs, "yaxs"=yaxs))
if (verbose) {
printDebug("plotPolygonDensity(): ",
"barCol, polyCol:",
c(barCol, polyCol),
fgText=list("orange", "dodgerblue", c(barCol, polyCol)));
}
#if (barCol %in% "#00337799" && polyCol == "#00449977" && length(col) > 0) {
if (barCol %in% formals(plotPolygonDensity)$barCol &&
polyCol %in% formals(plotPolygonDensity)$polyCol &&
length(col) > 0) {
barCol <- makeColorDarker(col,
fixAlpha=colAlphas[1],
darkFactor=darkFactors[1]);
polyCol <- makeColorDarker(col,
fixAlpha=colAlphas[2],
darkFactor=darkFactors[2]);
polyBorder <- makeColorDarker(col,
fixAlpha=colAlphas[3],
darkFactor=darkFactors[3]);
}
if (xScale %in% "default") {
if ("x" %in% log) {
xScale <- "log10";
}
}
## Optional visual log-transformation
#if ("x" %in% log) {
if ("log10" %in% xScale) {
x <- log10(abs(x) + 1) * sign(x);
xLogAxisType <- attr(x, "xLogAxisType");
} else if ("sqrt" %in% xScale) {
x1 <- x;
## use square root of absolute value, multiplied by the sign
x <- sqrt(abs(x1)) * sign(x1);
}
if (doHistogram) {
if (removeNA) {
x <- rmNA(x);
}
if (add) {
hx <- call_fn_ellipsis(graphics::hist.default,
x=x,
breaks=breaks,
col=barCol,
main=main,
border=histBorder,
xaxt="n",
yaxt="n",
las=las,
# xlab="",
ylab="",
cex.axis=cex.axis*0.8,
add=add,
...);
} else {
hx <- call_fn_ellipsis(graphics::hist.default,
x=x,
breaks=breaks,
plot=FALSE,
...);
## Optionally define the y-axis scale
if (length(ylim) == 0 &&
length(ylimQuantile) > 0 &&
ylimQuantile < 1 &&
ylimQuantile > 0 &&
max(hx$counts) > 0) {
ylim <- c(0,
stats::quantile(hx$counts, c(ylimQuantile)));
}
if (length(xlim) == 0) {
xlim <- range(hx$breaks, na.rm=TRUE);
}
plot(hx,
col=barCol,
main=main,
border=histBorder,
xaxt="n",
yaxt=yaxt,
xaxs=xaxs,
yaxs=yaxs,
las=las,
ylab=ylab,
xlab=xlab,
cex.axis=cex.axis*0.8,
add=add,
ylim=ylim,
xlim=xlim,
...);
}
if (verbose) {
printDebug("plotPolygonDensity(): ",
"hx$breaks:",
format(trim=TRUE,
digits=2,
c(head(hx$breaks),
NA,
tail(hx$breaks))));
}
if (!add) {
if (!"n" %in% xaxt) {
if ("log10" %in% xScale) {
if (verbose) {
printDebug("plotPolygonDensity(): ",
"log10 x-axis scale.");
}
minorLogTicksAxis(1,
doMinorLabels=TRUE,
logBase=10,
displayBase=10,
offset=1,
logAxisType=xLogAxisType,
...);
} else if ("sqrt" %in% xScale) {
if (verbose) {
printDebug("plotPolygonDensity(): ",
"sqrt xScale");
}
atPretty <- sqrtAxis(side=1,
plot=FALSE);
axisFunc(1,
at=atPretty,
labels=names(atPretty),
las=las,
cex.axis=cex.axis,
...);
} else {
#atPretty <- pretty(hx$breaks, u5.bias=u5.bias, n=pretty.n, ...);
## Slight change to use the plot region instead of the histogram region
atPretty <- pretty(graphics::par("usr")[1:2],
u5.bias=u5.bias,
n=pretty.n,
...);
#graphics::axis(1, at=atPretty, labels=atPretty, las=las, ...);
axisFunc(1,
at=atPretty,
las=las,
cex.axis=cex.axis,
...);
if (verbose) {
printDebug("plotPolygonDensity(): ",
"atPretty: ",
atPretty);
}
}
}
graphics::box(bty=bty);
}
} else {
hx <- NULL;
}
## Calculate density
if (doPolygon) {
dx <- breakDensity(x=x,
bw=bw,
width=width,
breaks=breaks,
densityBreaksFactor=densityBreaksFactor,
weightFactor=weightFactor,
addZeroEnds=TRUE,
verbose=verbose,
...);
if (doHistogram) {
maxHistY <- max(hx$counts, na.rm=TRUE);
maxHistY <- graphics::par("usr")[4];
if (verbose) {
printDebug("plotPolygonDensity(): ",
"Re-scaled y to histogram height maxHistY:",
maxHistY);
}
## Scale the y-axis to match the histogram
xout <- (head(hx$breaks, -1) + tail(hx$breaks, -1))/2;
xu <- match(unique(dx$x), dx$x);
dy <- stats::approx(x=dx$x[xu], y=dx$y[xu], xout=xout)$y;
dScale <- stats::median(hx$counts[hx$counts > 0] / dy[hx$counts > 0]);
dx$y <- dx$y * dScale;
#dx$y <- normScale(dx$y,
# from=0,
# to=maxHistY*heightFactor);
}
if (verbose) {
printDebug("plotPolygonDensity(): ",
"Completed density calculations.");
}
} else {
dx <- NULL;
}
if (!doHistogram) {
plot(dx,
col="transparent",
main=main,
xaxt="n",
yaxt=yaxt,
xaxs=xaxs,
yaxs=yaxs,
...);
if (!"n" %in% xaxt) {
if ("log10" %in% xScale) {
minorLogTicksAxis(1,
logBase=10,
displayBase=10,
offset=1,
doMinorLabels=TRUE,
logAxisType=xLogAxisType,
...);
} else if ("sqrt" %in% xScale) {
if (verbose) {
printDebug("plotPolygonDensity(): ",
"sqrt xScale");
}
atPretty <- sqrtAxis(side=1,
plot=FALSE);
axisFunc(1,
at=sqrt(abs(atPretty))*sign(atPretty),
labels=names(atPretty),
las=las,
cex.axis=cex.axis,
...);
} else {
#atPretty <- pretty(hx$breaks, u5.bias=u5.bias, n=pretty.n, ...);
## Slight change to use the plot region instead of the histogram region
atPretty <- pretty(graphics::par("usr")[1:2],
u5.bias=u5.bias,
n=pretty.n,
...);
axisFunc(1,
at=atPretty,
las=las,
cex.axis=cex.axis,
...);
if (verbose) {
printDebug("plotPolygonDensity(): ",
"atPretty: ",
atPretty);
}
}
}
}
if (doPolygon) {
graphics::polygon(dx,
col=polyCol,
border=polyBorder,
lwd=lwd,
...);
}
## Optionally plot highlightPoints
if (doHistogram) {
if (length(highlightPoints) > 0) {
if (verbose) {
printDebug("plotPolygonDensity(): ",
"Plotting highlightPoints.");
}
hxh <- call_fn_ellipsis(graphics::hist.default,
x=x[highlightPoints],
breaks=hx$breaks,
col=highlightCol,
main="",
border=makeColorDarker(highlightCol),
xaxt="n",
yaxt="n",
ylab="",
# xlab="",
add=TRUE,
...);
}
}
if (!is.null(ablineV)) {
call_fn_ellipsis(graphics::abline,
v=ablineV,
col=ablineVcol,
lty=ablineVlty,
...);
}
if (!is.null(ablineH)) {
call_fn_ellipsis(graphics::abline,
h=ablineH,
col=ablineHcol,
lty=ablineHlty,
...);
}
invisible(list(d=dx,
hist=hx,
barCol=barCol,
polyCol=polyCol,
polyBorder=polyBorder,
histBorder=histBorder));
}
}
#' Calculate more detailed density of numeric values
#'
#' Calculate more detailed density of numeric values
#'
#' This function is a drop-in replacement for `stats::density()`,
#' simply to provide a quick alternative that defaults to a higher
#' level of detail. Detail can be adjusted using `densityBreaksFactor`,
#' where higher values will use a wider step size, thus lowering
#' the detail in the output.
#'
#' Note that the density height is scaled by the total number of points,
#' and can be adjusted with `weightFactor`. See Examples for how to
#' scale the y-axis range similar to `stats::density()`.
#'
#' @family jam practical functions
#'
#' @param x numeric vector
#' @param breaks numeric breaks as described for `stats::density()` except
#' that single integer value is multiplied by `densityBreaksFactor`.
#' @param bw character name of a bandwidth function, or NULL.
#' @param width NULL or numeric value indicating the width of breaks to
#' apply.
#' @param densityBreaksFactor numeric factor to adjust the width of
#' density breaks, where higher values result in less detail.
#' @param weightFactor optional vector of weights `length(x)` to apply
#' to the density calculation.
#' @param addZeroEnds logical indicating whether the start and end value
#' should always be zero, which can be helpful for creating a polygon.
#' @param baseline optional numeric value indicating the expected baseline,
#' which is typically zero, but can be set to a higher value to indicate
#' a "noise floor".
#' @param floorBaseline logical indicating whether to apply a noise floor
#' to the output data.
#' @param verbose logical indicating whether to print verbose output.
#' @param ... additional parameters are sent to `stats::density()`.
#'
#' @returns `list` output equivalent to `stats::density()`:
#' * `x`: The `n` coordinates of the points where the density is
#' estimated.
#' * `y`: The estimated density values, non-negative, but can be zero.
#' * `bw`: The bandidth used.
#' * `n`: The sample size after elimination of missing values.
#' * `call`: the call which produced the result.
#' * `data.name`: the deparsed name of the `x` argument.
#' * `has.na`: `logical` for compatibility, and always `FALSE`.
#'
#' @examples
#' x <- c(stats::rnorm(15000),
#' stats::rnorm(5500)*0.25 + 1,
#' stats::rnorm(12500)*0.5 + 2.5)
#' plot(stats::density(x))
#'
#' plot(breakDensity(x))
#'
#' plot(breakDensity(x, densityBreaksFactor=200))
#'
#' # trim values to show abrupt transitions
#' x2 <- x[x > 0 & x < 4]
#' plot(stats::density(x2), lwd=2)
#' lines(breakDensity(x2, weightFactor=1/length(x2)/10), col="red")
#' graphics::legend("topright", c("stats::density()", "breakDensity()"),
#' col=c("black", "red"), lwd=c(2, 1))
#'
#' @export
breakDensity <- function
(x,
breaks=length(x)/3,
bw=NULL,
width=NULL,
densityBreaksFactor=3,
weightFactor=1,
addZeroEnds=TRUE,
baseline=0,
floorBaseline=FALSE,
verbose=FALSE,
...)
{
## Purpose is to provide slightly more granular stats::density() than
## the default provides
##
## bw can be a custom density kernel, see stats::density() for details
##
## width can be the width supplied to stats::density() but if NULL
## then this function calculates a reasonable width based upon
## the data content
##
## densityBreaksFactor is used to tune the level of detail, roughly
## defines a smoothing window of roughly this many fold above the
## distance between breaks, using breaks either as an integer number
## of breaks, or as a discrete set of breakpoints.
## Set to 0.01 to see discrete values as sharp peaks, or
## set to 10 to see a smooth gradient.
##
## addZeroEnds=TRUE will add a baseline y-value to the beginning and end
## which helps when used to draw a polygon, using the baseline parameter.
##
## floorBaseline=TRUE will change any value below the baseline to the baseline
if (length(weightFactor) > 0) {
weightFactor <- rep(weightFactor, length.out=length(x));
}
if (is.null(bw)) {
if (is.null(width)) {
if (length(breaks) == 1) {
width <- diff(range(x))/(breaks) * densityBreaksFactor;
} else {
width <- diff(range(x)) /
(stats::median(diff(breaks))) * densityBreaksFactor;
}
}
if (verbose) {
printDebug("breakDensity(): ",
"width:",
formatC(width, format="g", digits=3));
printDebug("breakDensity(): ",
"breaks:",
formatC(breaks, format="g", digits=3));
}
dx <- stats::density(x,
width=width,
subdensity=TRUE,
weight=weightFactor,
...);
} else {
dx <- stats::density(x,
width=width,
subdensity=TRUE,
weight=weightFactor,
bw=bw,
...);
}
## Optionally add a y-value of zero to the beginning and end
if (addZeroEnds) {
if (verbose) {
printDebug("breakDensity(): ",
"Adding baseline:", baseline, " to ends.");
}
dx$x <- c(head(dx$x, 1), dx$x, tail(dx$x, 1));
dx$y <- c(baseline, dx$y, baseline);
}
## Optionally floor data at the baseline
if (floorBaseline && any(dx$y) < baseline) {
if (verbose) {
printDebug("breakDensity(): ",
"Enforcing a noise floor:", baseline);
}
dx$y[dx$y < baseline] <- baseline;
}
return(dx);
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.