#'
#' jamba: Jam Base Methods
#'
#' The jamba package contains several jam base functions
#' which are re-usable for routine R analysis work, and are
#' important dependencies for other Jam R packages.
#'
#' The goal will be to
#' maintain these methods as lightweight as possible, so
#' their inclusion in an analysis workflow will not incur
#' a noticeable burden.
#'
#' The sections and functions below are not comprehensive, but
#' provide examples of useful functions. The most highly-used functions
#' are: [printDebug()], [vigrep()], [nameVector()] with [makeNames()],
#' [pasteByRow()], [showColors()].
#'
#' @section plot functions:
#' * Enhanced [graphics::smoothScatter()] with [plotSmoothScatter()].
#' * Enhanced [graphics::image()] with [imageDefault()]
#' for rasterized heatmaps that preserve aspect ratio for non-square
#' images; [imageByColors()] for `data.frame` of colors and optional
#' labels, which by default places unique labels centered within a block of
#' repeated values.
#' * Quick color display [showColors()] for vector or list of color vectors.
#' * Quick blank plot [nullPlot()] with optional labeling of margins.
#' * Log-scaled axis labels `minorLogTicksAxis()`.
#' * Text labels using a border outline [shadowText()] for visible contrast.
#' * Base plot wrappers [getPlotAspect()], [decideMfrow()].
#'
#' @section string functions:
#' * Alphanumeric sort with [mixedSort()], [mixedOrder()], [mixedSortDF()]
#' * Custom wrappers to [grep()] for value-return [vgrep()], [vigrep()];
#' case-insensitive pattern search [igrep()], [vigrep()];
#' and grep with an ordered vector of patterns [provigrep()], [proigrep()].
#' * Name manipulations: make unique names with defined syntax
#' [makeNames()]; applying names to a vector [nameVector()];
#' named vector of names [nameVectorN()] useful with [lapply()].
#' * Row-wise concatenation from `data.frame` or `matrix` [pasteByRow()]
#' optionally skipping blank values; list to matrix without filling
#' missing values [rbindList()].
#' * Sorted [base::table()] with optional filter [tcount()].
#'
#' @section color functions:
#' * Color interconversion functions designed to be reversible, e.g.
#' [col2hcl()] and [col2hcl()].
#' * Set text contrast color for labels on colored background
#' [setTextContrastColor()].
#' * Color wrapper functions [makeColorDarker()], [getColorRamp()],
#' [showColors()].
#'
#' @section miscellaneous helper functions:
#' * Colored text output [printDebug()], colored R prompt [setPrompt()],
#' vectorized text styling [make_styles()].
#' * Interconversion from degrees to radians [deg2rad()], [rad2deg()].
#' * Simple date string functions [getDate()], [asDate()],
#' [dateToDaysOld()], [isDate()], [fileInfo()].
#' * Padding character strings or integers with leading or trailing
#' values [padString()], [padInteger()].
#' * Removing or editing missing values in place: [rmNA()],
#' [rmNULL()], [rmInfinite()].
#'
#' @section Jam options:
#' The `jamba` package recognizes some global options, but limits these
#' options to include only non-analysis options. For example, no global
#' option should change the numerical manipulation of data.
#' * `jam.lightMode` - boolean, defines whether the text background
#' is light (`TRUE` is bright) or dark (`FALSE` is dark) mainly for the
#' purpose of restricting text output colors from `printDebug` so
#' they have visible contrast.
#' * `jam.adjustRgb` - numerical setting used as a small adjustment of
#' colors used by the `crayon` functions to produce ANSI color text.
#'
#' @family jam practical functions
#'
#' @docType package
#' @name jamba
NULL
#' file information in data.frame format
#'
#' file information in data.frame format
#'
#' This function is a minor extension to `file.info()` in that it
#' adds the filename as a proper colname, and "size" which contains a text
#' file size.
#'
#' @return `data.frame` with file information, including "filename" and "size"
#' as additional colnames as compared to `file.info()` output.
#'
#' @param fileList character vector with one or more file paths.
#'
#' @family jam practical functions
#'
#' @export
fileInfo <- function
(fileList,
...)
{
# Purpose is to wrapper file.info() so it returns a pretty tabular
# summary for one or more files.
# One cool pattern to follow is to list files contained within a package:
# colsHead(fileInfo(list.files(path=find.package("jamba"), full.names=TRUE)))
fileList <- path.expand(fileList);
fi1 <- file.info(fileList, ...);
# convert size to readable label
fi1[,"size"] <- asSize(fi1[,"size"]);
fi1 <- data.frame(
check.names=FALSE,
stringsAsFactors=FALSE,
"filename"=rownames(fi1),
fi1);
# Left-justify the text by right-padding with spaces
# making it easier to read
fi1$filename <- format(
fi1$filename,
justify="left")
return(fi1);
}
#' prefix integers with leading zeros
#'
#' prefix integers with leading zeros
#'
#' The purpose of this function is to pad integer numbers so they contain
#' a consistent number of digits, which is helpful when sorting values
#' as character strings.
#'
#' @return `character` vector of length(x).
#'
#' @family jam string functions
#'
#' @param x `integer`, `numeric`, or `character` vector. In reality, only
#' `nchar(x)` is required to determine padding.
#' @param padCharacter `character` with nchar(padCharacter)==1, used to pad
#' each digit as a prefix.
#' @param useNchar `NULL` or `integer` number of digits used, or if the maximum
#' `nchar(x)` is higher, that number of digits is used. Note `useNchar` is
#' mostly useful when all numbers are less than 10, but the desired output
#' is to have a fixed number of digits 2 or higher.
#' @param ... additional parameters are ignored.
#'
#' @export
padInteger <- function
(x,
padCharacter="0",
useNchar=NULL,
...)
{
## Purpose is to pad integer numbers so they contain the same number of
## characters. This function is optimized to be (hopefully) faster than
## padString().
if (length(x) == 0) {
return(x);
}
maxNchar <- max(c(useNchar, nchar(x)));
if (maxNchar == 0) {
return("");
}
x1 <- paste(paste(rep(padCharacter, maxNchar), collapse=""), x, sep="");
x2 <- substr(x1, nchar(x1)-maxNchar+1, nchar(x1));
return(x2);
}
#' pad a character string to a fixed length
#'
#' pad a character string to a fixed length
#'
#' @return `character` vector of length(x)
#'
#' @family jam string functions
#'
#' @param x `character` vector
#' @param stringLength `integer` length for the resulting character strings
#' in `x`. By default, all strings are padded to the length of the
#' longest entry, however stringLength can be defined to impose strict
#' number of characters for all entries.
#' @param padCharacter `character` string with nchar=1 used for padding.
#' @param justify `character` string with "left", "right", "center" to indicate
#' alignment of the resulting text string.
#' @param ... additional parameters are ignored.
#'
#' @examples
#' padString(c("one","two","three"));
#' padString(c("one","two","three","four"), padCharacter="_", justify="center");
#'
#' @export
padString <- function
(x,
stringLength=max(nchar(x)),
padCharacter=" ",
justify="left",
...)
{
## Purpose is to add padding characters (spaces) to force a string to
## be a certain fixed number of characters wide
## Note: It will also crop strings to this same length in case the
## given string(s) are too long.
if (length(stringLength) > 1) {
x <- substring(x, 1, stringLength);
} else {
x <- substr(x, 1, stringLength);
}
if (justify == "right") {
x <- sapply(x, function(i){
paste(c(rep(padCharacter, stringLength-nchar(i)), i), collapse="");
})
} else if (justify == "center") {
x <- sapply(x, function(i){
n1 <- ceiling((stringLength-nchar(i))/2);
n2 <- floor((stringLength-nchar(i))/2);
paste(c(rep(padCharacter, n1),
i,
rep(padCharacter, n2)), collapse="");
})
} else {
## Everything else gets left-justified
x <- sapply(x, function(i){
paste(c(i, rep(padCharacter, stringLength-nchar(i))), collapse="");
})
}
return(x);
}
#' convert date to age in days
#'
#' convert date to age in days
#'
#' @family jam date functions
#'
#' @return integer value with the number of calendar days before the
#' current date, or the `nowDate` if supplied.
#'
#' @param testDate `character` date recognized by `asDate()`,
#' representing the test date.
#' @param nowDate `character` date recognized by `asDate()`,
#' representing the reference date, by default the current day.
#' @param units `character` indicating the units, as used by
#' `difftime()`.
#' @param ... additional parameters are ignored.
#'
#' @examples
#' dateToDaysOld("23aug2007")
#'
#' @export
dateToDaysOld <- function
(testDate,
nowDate=Sys.Date(),
units="days",
...)
{
## Purpose is to report the number of days old something is,
## using the asDate() format "DDmmmYYYY" like "03may1997"
as.integer(difftime(nowDate, asDate(testDate), units=units));
}
#' convert date DDmmmYYYY to Date
#'
#' convert date DDmmmYYYY to Date
#'
#' This function converts a text date string to Date object, mainly to
#' allow date-related math operations, for example \code{\link[base]{difftime}}.
#'
#' @family jam date functions
#'
#' @return Date object
#'
#' @param getDateValues `character` date, in format recognized by dateFormat
#' @param dateFormat `character` string representing the recognized date
#' format, by default `"DDmmmYYYY"`, which recognizes `"23aug2007"`.
#' @param ... additional parameters are ignored.
#'
#' @examples
#' asDate(getDate());
#'
#' @export
asDate <- function
(getDateValues,
dateFormat="%d%b%Y",
...)
{
## Purpose is to convert getDate() formatted values "21jan2012"
## into proper R Date objects for sorting purposes
##
## dateFormat is the output format
newDates <- as.Date(getDateValues, dateFormat);
names(newDates) <- getDateValues;
return(newDates);
}
#' get simple date string
#'
#' get simple date string in the format DDmonYYYY such as 17jul2018.
#'
#' Gets the current date in a simplified text string. Use
#' `asDate()` to convert back to Date object.
#'
#' @return character vector with simplified date string
#'
#' @family jam date functions
#'
#' @param t current time in an appropriate class such as `"POSIXct"`
#' or `"POSIXt"`. The default is output of `Sys.time()`.
#' @param trim `logical` whether to trim the output of `format()`
#' in the event that multiple values are sent for argument `t`.
#' @param dateFormat `character` string representing the recognized date
#' format, by default `"DDmmmYYYY"`, which recognizes `"23aug2007"`.
#' @param ... additional parameters sent to `format()`.
#'
#' @examples
#' getDate();
#'
#' @export
getDate <- function
(t=Sys.time(),
trim=TRUE,
dateFormat="%d%b%Y",
...)
{
## Purpose is to define a data in the format
## 05may2011 (DDmmmYYYY)
tolower(
format(t,
dateFormat,
trim=trim,
...));
}
#' set R prompt with project name and R version
#'
#' set R prompt with project name and R version
#'
#' This function sets the R prompt including project name, the R
#' version, and the process ID. It is intended to be useful by
#' reinforcing the active project for an R session, particularly when
#' there may be multiple R sessions active. The R version can be useful
#' when running R on different machines, to reinforce which version of
#' R is active on the given machine. The process ID is mainly helpful in
#' the event an R process spins out of control, and it would be useful
#' to know definitively which exact process ID is stuck, so that it can
#' be killed without affecting other R sessions inadvertently.
#'
#' The prompt is defined in `options("prompt")`.
#'
#' Note that in some cases, the color encoding of the prompt interferes
#' with word wrapping, the symptom is that when typing text into the R console
#' a long line will begin to word wrap prematurely, before the text reaches
#' the edge of the screen. There are two frequent causes of this issue:
#'
#' \describe{
#' \item{options("width")}{is sometimes defined too narrow for the
#' screen, which can happen when resizing the console, or when
#' accessing an R session via GNU screen, or tmux, and the environment
#' variable has not been propagated to the terminal window. Usually
#' this issue is resolved by defining `options("width")` manually,
#' or by simply resizing the terminal window, which may trigger the
#' appropriate environment variable updates.}
#' \item{The locale}{can sometimes be mismatched with the terminal window,
#' usually caused by some terminal emulation layer which is not
#' properly detecting the compatibility of the server. It may happen
#' for example, when using PuTTY on Windows, or when using GNU screen or
#' tmux on linux or Mac OSX. To troubleshoot, check
#' `Sys.env("LC_ALL")` which may be `"C"` or another locale such as
#' `"en_US.UTF-8"`. Note that switching locale may have the effect of
#' correcting the word wrap, but may adversely affect display of
#' non-standard unicode characters.}
#' }
#'
#' In any event, R uses readline for unix-like systems by default, and
#' issues related to using color prompt are handled at that level. For example,
#' in some Mac OSX consoles, there are alternate color escape sequences which
#' are used to tell readline to ignore an escape sequence when it counts the
#' number of characters being displayed by the prompt.
#'
#' @return `character` string representing the prompt used, returned
#' invisibly.
#'
#' @family jam practical functions
#'
#' @param projectName `character` string representing the active project.
#' @param useColor `logical` whether to define a color prompt if the
#' `crayon` package is installed.
#' @param projectColor,bracketColor,Rcolors,PIDcolor,promptColor `character`
#' colors used when `useColor==TRUE` and the `crayon` package
#' is installed:
#' * `projectColor` colors the project name;
#' * `bracketColor` colors the curly brackets around the project;
#' * `Rcolors` can be a vector of 3 colors, colorizing "R",
#' the "-" divider, and the R version;
#' * `PIDcolor` colors the PID when `usePid=TRUE`; and
#' * `promptColor` colors the `">"` at the end of the prompt.
#' @param usePid `logical` whether to include the process ID in the prompt.
#' Including the PID is helpful for the rare occasion when a process is
#' hung and needs to be stopped directly.
#' @param resetPrompt `logical` whether to revert all changes to the prompt
#' back to the default R prompt, that is, no color and no projectName.
#' @param addEscape `logical` or `NULL` indicating whether to wrap color
#' encoding ANSI inside additional escape sequences. This change is
#' helpful for linux-based (readline-based) R consoles, by telling
#' the console not to count ANSI color control characters as visible
#' characters when determining word wrapping on the console. Note
#' that RStudio does not work well with this setting.
#' If you find that the word-wrap is incorrect in the R console, try
#' `addEscape=TRUE`. Apparently most versions of RStudio will already
#' adjust (and prevent) colorizing the prompt during editing, presumably
#' to sidestep the problem of calculating the correct character length.
#' By default when `addEscape` is `NULL`, it checks whether environmental
#' variable `RSTUDIO` equals `"1"` (running inside RStudio) then sets
#' `addEscape=FALSE`; otherwise it defines `addEscape=TRUE`.
#' In most cases for commandline prompts, `addEscape=TRUE` is helpful
#' and not problematic.
#' @param verbose `logical` whether to print verbose output.
#' @param debug `logical` indicating whether to print the ANSI control
#' character output for the full prompt, for visual review.
#' @param ... additional parameters are passed to `make_styles()` which is
#' only relevant with the argument `useColor=TRUE`.
#'
#' @examples
#' \dontrun{
#' setPrompt("jamba", projectColor="purple");
#' setPrompt("jamba", usePid=FALSE);
#' }
#'
#' @export
setPrompt <- function
(projectName=NULL,
useColor=TRUE,
projectColor="yellow",
bracketColor="white",
Rcolors=c("white","white","white"),
PIDcolor=NA,
promptColor="white",
usePid=TRUE,
resetPrompt=FALSE,
addEscape=NULL,
debug=FALSE,
verbose=FALSE,
...)
{
## Set the R command prompt to display the current R project name
##
## usePid will include the parent process PID in the prompt, which
## can be helpful when an R session hangs, but you have multiple active
## R sessions, and might otherwise not be able to tell which R session
## is problematic.
##
## if projectName is not supplied, try .GlobalEnv
if (length(projectName) == 0) {
if (exists("projectName", envir=.GlobalEnv)) {
projectName <- get("projectName", envir=.GlobalEnv);
} else {
projectName <- "unnamed";
}
}
if (length(useColor) > 0 &&
useColor &&
check_pkg_installed("crayon")) {
useColor <- 1;
} else {
useColor <- 0;
}
promptValue <- "> ";
projectColor <- rep(c(projectColor, "white"), length.out=1);
bracketColor <- rep(c(bracketColor, "white"), length.out=1);
Rcolors <- rep(c(rep(c(Rcolors), length.out=3), "white"), length.out=3);
PIDcolor <- rep(c(PIDcolor, NA), length.out=1);
promptColor <- rep(c(promptColor, "white"), length.out=1);
if (verbose) {
printDebug("setPrompt(): ",
"useColor:",
useColor);
}
if (resetPrompt) {
if (verbose) {
printDebug("setPrompt(): ",
"Resetting basic prompt for R.");
}
options("prompt"="> ");
} else if (useColor == 1) {
## use crayon
if (!usePid) {
promptValue <- paste(
make_styles(
style=c(bracketColor,
projectColor,
bracketColor,
NA,
Rcolors,
promptColor),
c("{",
projectName,
"}",
"-",
"R",
"-",
paste0(R.version[c("major", "minor")], collapse="."),
"> "),
verbose=verbose,
...
),
collapse="");
} else {
promptValue <- paste(
make_styles(
style=c(bracketColor,
projectColor,
bracketColor,
NA,
Rcolors,
NA,
PIDcolor,
promptColor),
c("{",
projectName,
"}",
"-",
"R",
"-",
paste0(R.version[c("major", "minor")], collapse="."),
"_",
Sys.getpid(),
"> "),
verbose=verbose,
...
),
collapse="");
}
} else {
if (verbose) {
printDebug("Setting non-colorized prompt for R.");
}
if (!usePid) {
promptValue <- paste0("{",
projectName,
"}",
"-R-",
paste(R.version[c("major", "minor")], collapse="."),
"> ");
} else {
promptValue <- paste0("{",
projectName,
"}",
"-R-",
paste(R.version[c("major", "minor")], collapse="."),
"_",
Sys.getpid(),
"> ");
}
}
## optionally add escape sequences
if (length(addEscape) == 0) {
if (Sys.getenv("RSTUDIO") %in% "1") {
addEscape <- FALSE;
} else {
addEscape <- TRUE;
}
}
if (useColor && addEscape) {
promptValue <- gsub("(\033[[0-9;]+[nm]{0,1})",
"\001\\1\002",
promptValue);
}
if (debug) {
cat("promptValue:\n");
print(promptValue);
}
if (verbose) {
cat("setPrompt() defined promptValue: '", promptValue, "'\n\n");
}
options("prompt"=promptValue);
invisible(promptValue);
}
#' Paste data.frame rows into character vector
#'
#' Paste data.frame rows into a character vector, optionally removing
#' empty fields in order to avoid delimiters being duplicated.
#'
#' This function is intended to paste `data.frame` (or `matrix`, or `tibble`)
#' values for each row of data.
#' It differs from using `apply(x, 2, paste)`:
#'
#' * it handles factors without converting to integer factor level numbers.
#' * it also by default removes blank or empty fields, preventing the delimiter
#' from being included multiple times, per the `condenseBlanks` argument.
#' * it is notably faster than apply, by means of running `paste()` on
#' each column of data, making the output vectorized, and scaling rather
#' well for large `data.frame` objects.
#'
#' The output can also include name:value pairs, which can make the output
#' data more self-describing in some circumstances. That said, the most basic
#' usefulness of this function is to create row labels.
#'
#' @return `character` vector of length `nrow(x)`.
#'
#' @family jam string functions
#'
#' @param x `data.frame` or comparable object such as `matrix` or `tibble`.
#' @param sep `character` string separator to use between columns.
#' @param na.rm `logical` whether to remove NA values, or include them
#' as `"NA"` strings.
#' @param condenseBlanks `logical` whether to condense blank or empty values
#' without including an extra delimiter between columns.
#' @param includeNames `logical` whether to include the colname delimited
#' prior to the value, using sepName as the delimiter.
#' @param sepName `character` string relevant when `includeNames=TRUE`,
#' this value becomes the delimiter between name:value.
#' @param blankGrep `character` string used as regular expression pattern in
#' `grep()` to recognize blank entries;
#' by default any field containing no text, or only whitespace,
#' is considered a blank entry.
#' @param verbose `logical` whether to print verbose output.
#' @param ... additional arguments are ignored.
#'
#' @examples
#' # create an example data.frame
#' a1 <- c("red","blue")[c(1,1,2)];
#' b1 <- c("yellow","orange")[c(1,2,2)];
#' d1 <- c("purple","green")[c(1,2,2)];
#' df2 <- data.frame(a=a1, b=b1, d=d1);
#' df2;
#'
#' # the basic output
#' pasteByRow(df2);
#'
#' # Now remove an entry to show the empty field is skipped
#' df2[3,3] <- "";
#' pasteByRow(df2);
#'
#' # the output tends to make good rownames
#' rownames(df2) <- pasteByRow(df2);
#'
#' # since the data.frame contains colors, we display using
#' # imageByColors()
#' par("mar"=c(5,10,4,2));
#' imageByColors(df2, cellnote=df2);
#'
#' @export
pasteByRow <- function
(x,
sep="_",
na.rm=TRUE,
condenseBlanks=TRUE,
includeNames=FALSE,
sepName=":",
blankGrep="^[ ]*$",
verbose=FALSE,
...)
{
## Purpose is to paste values in each column using paste(),
## intending to be faster than apply(x, 1, paste), while also
## allowing some logic about how to handle missing values.
##
## x can be a data.frame, tibble, matrix, or DataFrame.
##
## Converts factor columns to character to prevent inadvertent
## use of integers which are pointers to factor levels.
##
## condenseBlanks=TRUE will remove blank entries so there are no two
## delimiters back to back
##
## includeNames=TRUE will include the colnames along with values,
## typically useful for generating name:value pairs. When includeNames=TRUE
## the sepName is used to delimit name:value pairs. The name:value pair
## is then delimited using sep.
##
## To use this function to create rownames or vector names, it is often
## helpful to run makeNames() to ensure names are unique, e.g.
## rownames(x) <- makeNames(pasteByRow(x))
##
## TODO: revisit how to handle na.rm=FALSE, where keeping "NA" would
## be beneficial.
##
sep <- head(sep, 1);
if (length(ncol(x)) == 0 || ncol(x) == 0) {
return(x);
}
## Convert matrix to data.frame, so we can use consistent [[x]] syntax
## Note: consider avoiding this converstion in order to save memory,
## potentially helpful for objects with large number of rows
if (igrepHas("matrix", class(x))) {
x <- as.data.frame(x);
}
## Convert factor columns to character
for (iCol in seq_len(ncol(x))) {
if (igrepHas("factor", class(x[,iCol]))) {
x[,iCol] <- as.character(x[[iCol]]);
}
}
## Note: the use of [[1]] requires data.frame or tibble, and
## can no longer use matrix class directly
getColVals <- function(x, i, includeNames, na.rm, sepName) {
xVals <- x[[i]];
isNa <- (is.na(xVals));
if (any(isNa)) {
if (na.rm) {
xVals[isNa] <- "";
} else {
xVals[isNa] <- "NA";
}
}
if (condenseBlanks) {
isBlank <- grep(blankGrep, xVals);
}
if (includeNames) {
xVals <- paste0(colnames(x)[i], sepName, xVals);
if (condenseBlanks && length(isBlank) > 0) {
xVals[isBlank] <- "";
}
} else {
if (condenseBlanks && length(isBlank) > 0) {
xVals[isBlank] <- "";
}
}
xVals;
}
xVals <- getColVals(x, 1, includeNames, na.rm, sepName);
if (ncol(x) > 1) {
for (i1 in 2:ncol(x)) {
xVals1 <- getColVals(x, i1, includeNames, na.rm, sepName);
if (condenseBlanks) {
isBlank1 <- (is.na(xVals1) | grepl(blankGrep, xVals1));
isBlank <- (is.na(xVals) | grepl(blankGrep, xVals));
sepV <- ifelse(isBlank | isBlank1, "", sep);
} else {
sepV <- sep;
}
xVals <- paste0(xVals, sepV, xVals1);
}
}
if (!is.null(rownames(x))) {
names(xVals) <- rownames(x);
}
return(xVals);
}
#' break a vector into groups
#'
#' breaks a vector into groups
#'
#' This function takes a vector of values, determines "chunks" of identical
#' values, from which it defines where breaks occur. It assumes the input
#' vector is ordered in the way it will be displayed, with some labels
#' being duplicated consecutively. This function defines the breakpoints
#' where the labels change, and returns the ideal position to put a single
#' label to represent a duplicated consecutive set of labels.
#'
#' It can return fractional coordinates, for example when a label represents
#' two consecutive items, the fractional coordinate can be used to place the
#' label between the two items.
#'
#' This function is useful for things like adding labels to
#' `imageDefault()` color image map of sample groupings, where
#' it may be ideal to label only unique elements in a contiguous set.
#'
#' @return
#' `list` with the following named elements:
#' \itemize{
#' \item{"breakPoints"}{The mid-point coordinate between each break.
#' These midpoints would be good for drawing dividing lines for
#' example.}
#' \item{"labelPoints"}{The ideal point to place a label to represent
#' the group.}
#' \item{"newLabels"}{A vector of labels the same length as the input
#' data, except using blank values except where a label should
#' be drawn. This output is good for text display.}
#' \item{"useLabels"}{The unique set of labels, without blanks,
#' corresponding to the coordinates supplied by labelPoints.}
#' \item{"breakLengths"}{The integer size of each set of labels.}
#' }
#'
#' @family jam string functions
#'
#' @param x `character` vector of labels
#' @param labels `character` vector of custom labels to represent the items
#' in x
#' @param returnFractions `logical` whether to return fractional coordinates
#' for labels that should be positioned between two labels
#' @param ... additional parameters are ignored.
#'
#' @examples
#' b <- rep(LETTERS[c(1:5, 1)], c(2,3,5,4,3,4));
#' bb <- breaksByVector(b);
#' # Example showing how labels can be minimized inside a data.frame
#' data.frame(b,
#' newLabels=bb$newLabels);
#'
#' # Example showing how to reposition text labels
#' # so duplicated labels are displayed in the middle
#' # of each group
#' bb2 <- breaksByVector(b, returnFractions=TRUE);
#' ylabs <- c("minimal labels", "all labels");
#' adjustAxisLabelMargins(ylabs, 2);
#' adjustAxisLabelMargins(bb2$useLabels, 1);
#' nullPlot(xlim=range(seq_along(b)), ylim=c(0,3),
#' doBoxes=FALSE, doUsrBox=TRUE);
#' axis(2, las=2, at=c(1,2), ylabs);
#' text(y=2, x=seq_along(b), b);
#' text(y=1, x=bb2$labelPoints, bb2$useLabels);
#'
#' ## Print axis labels in the center of each group
#' axis(3,
#' las=2,
#' at=bb2$labelPoints,
#' labels=bb2$useLabels);
#'
#' ## indicate each region
#' for (i in seq_along(bb2$breakPoints)) {
#' axis(1,
#' at=c(c(0, bb2$breakPoints)[i]+0.8, bb2$breakPoints[i]+0.2),
#' labels=c("", ""));
#' }
#' ## place the label centered in each region without adding tick marks
#' axis(1,
#' las=2,
#' tick=FALSE,
#' at=bb2$labelPoints,
#' labels=bb2$useLabels);
#' ## abline to indicate the boundaries, if needed
#' abline(v=c(0, bb2$breakPoints) + 0.5,
#' lty="dashed",
#' col="blue");
#'
#' # The same process is used by imageByColors()
#'
#' @export
breaksByVector <- function
(x,
labels=NULL,
returnFractions=FALSE,
...)
{
## Purpose is to take a vector of values, and determine the "chunks" of
## identical values, so we can define where the breaks occur.
## labels is expected to have length equal to the number of Rle chunks.
##
## breakPoints = the coordinate start for each break
## labelPoints = the midpoint coordinate between each break
## newLabels = vector of labels, where everything is empty except at the
## midpoint of each cluster, where labels are applied.
##
## returnFractions=TRUE will place a label between cells, in case there are an
## even number of duplicated entries, it will return the coordinate halfway
## between the middle.
##
## Use the base::rle() function instead of IRanges::Rle()
if (any(c("factor", "ordered") %in% class(x))) {
x <- nameVector(as.character(x),
names(x),
makeNamesFunc=c);
}
xRle <- rle(x);
## Commented out syntax used by Bioconductor Rle
#breakPoints <- cumsum(runLength(xRle));
xLengths <- xRle$lengths;
breakPoints <- cumsum(xRle$lengths);
useLabels <- xRle$values;
names(breakPoints) <- useLabels;
labelPoints <- 0.5 + (c(0, head(breakPoints, -1)) + breakPoints) / 2;
if (!returnFractions) {
labelPoints <- trunc(labelPoints);
}
names(labelPoints) <- names(breakPoints);
if (!is.null(labels) && length(labels) == length(breakPoints)) {
newLabels <- rep("", length(x));
newLabels[labelPoints] <- labels;
} else {
newLabels <- rep("", length(x));
#newLabels[labelPoints] <- runValue(xRle);
newLabels[labelPoints] <- xRle$values;
}
if (!is.null(names(x))) {
newLabels <- nameVector(newLabels, names(x));
}
# calculate chunk sizes
list(breakPoints=breakPoints,
labelPoints=labelPoints,
newLabels=newLabels,
useLabels=useLabels,
breakLengths=xLengths);
}
#' Draw grouped axis labels
#'
#' Draw grouped axis labels given a character vector.
#'
#' This function extends `breaksByVector()` specifically for
#' axis labels. It is intended where character labels are spaced
#' at integer steps, and some labels are expected to be repeated.
#'
#' @family jam plot functions
#'
#' @returns `data.frame` invisibly, which contains the relevant axis
#' coordinates, labels, and whether the coordinate should
#' appear with a tick mark.
#'
#' @examples
#' par("mar"=c(4,4,6,6));
#' b <- rep(LETTERS[1:5], c(2,3,5,4,3));
#' b2 <- c(b[1:2], makeNames(b[3:5]), b[6:16]);
#' nullPlot(doBoxes=FALSE,
#' doUsrBox=TRUE,
#' xlim=c(0,18),
#' ylim=c(0,18));
#'
#' groupedAxis(1, b);
#' groupedAxis(2, b, group_style="grouped");
#' groupedAxis(2, b, group_style="centered");
#' groupedAxis(3, b2, do_abline=TRUE);
#' groupedAxis(4, b2, group_style="grouped");
#' mtext(side=1, "group_style='partial_grouped'", line=2, las=0);
#' mtext(side=2, "group_style='grouped'", line=2, las=0);
#' mtext(side=3, "group_style='partial_grouped'", line=2, las=0);
#' mtext(side=4, "group_style='grouped'", line=2, las=0);
#'
#' @param side `integer` indicating the axis side, passed to `axis()`.
#' 1=bottom, 2=left, 3=top, 4=right.
#' @param x `character` vector of axis labels
#' @param group_style `character` string indicating the style of label:
#' * `"partial_grouped"` - uses square bracket to bound 2+ repeated entries,
#' and single line tick mark for non-repeated entries.
#' * `"grouped"` - uses square bracket to bound each set of repeated entries
#' including non-repeated entries.
#' * `"centered"` - only labels the center of each group of repeated entries
#' with no bracket bounding the entries.
#' @param las `integer` indicating whether labels should be perpendicular,
#' see `par("las")`.
#' @param returnFractions `logical` passed to `breaksByVector()` to calculate
#' label positions. Set `returnFractions=FALSE` and all labels will only
#' appear at integer locations on the axis.
#' @param nudge `numeric` adjustment for labels away from the plot border.
#' @param do_abline `logical` indicating whether to draw `abline()` lines
#' inside the plot to indicate the exact breakpoints between each group
#' of labels.
#' @param abline_lty line type compatible with `par("lty")`, used when
#' `do_abline=TRUE`.
#' @param abline_col `character` color used when `do_abline=TRUE`.
#' @param do_plot `logical` whether to plot the resulting axis,
#' as an option to suppress the output and do something else
#' with the `data.frame` of coordinates returned by this function.
#' @param ... additional arguments are passed to `breaksByVector()`, and/or to
#' `axis()`.
#'
#' @export
groupedAxis <- function
(side=1,
x,
group_style=c("partial_grouped",
"grouped",
"centered"),
las=2,
returnFractions=TRUE,
nudge=0.2,
do_abline=FALSE,
abline_lty="solid",
abline_col="grey40",
do_plot=TRUE,
...)
{
## Purpose is to provide a convenient wrapper for breaksByVector()
## used to draw grouped axis labels
sides <- intersect(c(1,2,3,4), side);
group_style <- match.arg(group_style);
## Call breaksByVector()
bb2 <- breaksByVector(x=x,
returnFractions=returnFractions,
...);
## Print axis labels in the center of each group
for (side in sides) {
if ("centered" %in% group_style) {
axis_df <- tryCatch({
data.frame(check.names=FALSE,
stringsAsFactors=FALSE,
axis_at=bb2$labelPoints,
axis_ticks=TRUE,
axis_labels=bb2$useLabels,
axis_side=side,
axis_group=1)
}, error=function(e){
data.frame(check.names=FALSE,
stringsAsFactors=FALSE,
axis_at=1,
axis_ticks=TRUE,
axis_labels="",
axis_side=1,
axis_group=1)[0, , drop=FALSE]
})
if (TRUE %in% do_plot) {
axis(side=side,
las=las,
at=bb2$labelPoints,
labels=bb2$useLabels,
...);
}
} else if (any(c("partial_grouped", "grouped") %in% group_style)) {
## indicate each region
axis_dfs <- lapply(seq_along(bb2$breakPoints), function(i){
x1 <- c(0, bb2$breakPoints)[i] + 1;
x2 <- bb2$breakPoints[i];
if (x1 == x2 && "partial_grouped" %in% group_style) {
axis_df <- data.frame(check.names=FALSE,
stringsAsFactors=FALSE,
axis_at=x1,
axis_ticks=TRUE,
axis_labels="",
axis_side=side,
axis_group=i)
if (TRUE %in% do_plot) {
axis(side=side,
at=x1,
labels=c(""),
...);
}
} else {
axis_df <- data.frame(check.names=FALSE,
stringsAsFactors=FALSE,
axis_at=c(
x1 - nudge,
x2 + nudge),
axis_ticks=TRUE,
axis_labels=c("",
""),
axis_side=side,
axis_group=i)
if (TRUE %in% do_plot) {
axis(side=side,
at=c(
x1 - nudge,
x2 + nudge),
labels=c("", ""),
...);
}
}
axis_df
})
axis_df1 <- tryCatch({
rbindList(axis_dfs);
}, error=function(e){
data.frame(check.names=FALSE,
stringsAsFactors=FALSE,
axis_at=1,
axis_ticks=TRUE,
axis_labels="",
axis_side=side,
axis_group=1)[0, , drop=FALSE]
})
## place the label centered in each region without adding tick marks
axis_df2 <- tryCatch({
data.frame(check.names=FALSE,
stringsAsFactors=FALSE,
axis_at=bb2$labelPoints,
axis_ticks=FALSE,
axis_labels=bb2$useLabels,
axis_side=side,
axis_group=seq_along(bb2$useLabels))
}, error=function(e){
data.frame(check.names=FALSE,
stringsAsFactors=FALSE,
axis_at=1,
axis_labels="",
axis_side=side,
axis_group=1)[0, , drop=FALSE]
})
axis_df <- mixedSortDF(
rbind(axis_df1, axis_df2),
byCols=c("axis_group", "axis_at"))
if (TRUE %in% do_plot) {
axis(side=side,
las=las,
tick=FALSE,
at=bb2$labelPoints,
labels=bb2$useLabels,
...);
}
}
}
## abline to indicate the boundaries, if needed
if (TRUE %in% do_abline && TRUE %in% do_plot) {
if (any(c(1,3) %in% sides)) {
abline(v=c(0, bb2$breakPoints) + 0.5,
lty=abline_lty,
col=abline_col,
...);
}
if (any(c(2,4) %in% sides)) {
abline(h=c(0, bb2$breakPoints) + 0.5,
lty=abline_lty,
col=abline_col,
...);
}
}
if (nrow(axis_df) > 0) {
rownames(axis_df) <- NULL
}
return(invisible(axis_df));
}
#' convert column number to Excel column name
#'
#' convert column number to Excel column name
#'
#' The purpose is to convert an `integer` column number into a valid Excel
#' column name, using `LETTERS` starting at A.
#' This function implements an arbitrary number of digits, which may or
#' may not be compatible with each version of Excel. 18,278 columns
#' would be the maximum for three digits, "A" through "ZZZ".
#'
#' This function is useful when referencing Excel columns via another
#' interface such as via openxlsx. It is also used by `makeNames()`
#' when the `numberStyle="letters"`, in order to provide letter suffix values.
#'
#' One can somewhat manipulate the allowed column names via the `useLetters`
#' argument, which by default uses the entire 26-letter Western alphabet.
#'
#' @return `character` vector with length(x)
#'
#' @family jam practical functions
#'
#' @param x `integer` vector
#' @param useLetters `character` vector of single-digit characters to use as
#' digits in the resulting column name. Note that these characters can
#' be of almost any length, with any content.
#' @param zeroVal `character` single-digit to be used whenever `x==0`, or as a
#' prefix for negative values. In theory there should be no negative
#' input values, but this basic mechanism is used to handle the possibility.
#'
#' @examples
#' colNum2excelName(1:30)
#'
#' @export
colNum2excelName <- function
(x,
useLetters=LETTERS,
zeroVal="a",
...)
{
## Purpose is to convert a numerical column number into Excel name.
## This function implements an arbitrary number of digits, which may or
## may not be compatible with each version of Excel. 18,278 columns
## would be the maximum for three digits, "A" through "ZZZ"
##
## Custom function which returns the remainder except instead
## of zero, it uses the maximum. This function is vectorized
xSign <- sign(x);
x <- abs(x);
subRemainder <- function(x, base=26, ...)
{
sub1 <- x %% base;
ifelse(sub1 == 0, base, sub1);
}
useBase <- length(useLetters);
## We must add a blank entry at the end, so paste() is not
## allowed to omit NULL entries in vectorized mode.
useLetters <- c(useLetters[1:useBase], "");
## Start with the last digit and work inward
sub1 <- subRemainder(x, base=useBase);
sub1vals <- useLetters[sub1];
main1 <- as.integer((x-1) / useBase);
while (any(main1 > useBase)) {
lg1 <- which(main1 > useBase);
sub2vals <- useLetters[subRemainder(main1[lg1], base=useBase)];
sub1vals[lg1] <- paste0(sub2vals, sub1vals[lg1]);
main1[lg1] <- as.integer((main1[lg1]-1) / useBase);
}
## We must change zero to the last entry which is ""
main1[main1 == 0] <- (useBase + 1);
main1vals <- useLetters[main1];
excelColName <- paste0(main1vals, sub1vals);
names(excelColName) <- names(x);
## Values of zero are set to zeroVal, by default "a"
if (any(x %in% 0)) {
excelColName[x %in% 0] <- zeroVal;
}
## Negative values are prefixed with zeroVal, by default "a"
if (any(xSign %in% "-1")) {
xNegative <- which(xSign %in% "-1");
excelColName[xNegative] <- paste0(zeroVal, excelColName[xNegative]);
}
return(excelColName);
}
#' Decide plot panel rows, columns for par(mfrow)
#'
#' Decide plot panel rows, columns for par(mfrow)
#'
#' This function returns the recommended rows and columns of panels
#' to be used in `par("mfrow")` with R base plotting. It attempts
#' to use the device size and plot aspect ratio to keep panels roughly
#' square. For example, a short-wide device would have more columns of panels
#' than rows; a tall-thin device would have more rows than columns.
#'
#' The `doTest=TRUE` argument will create `n` number of
#' panels with the recommended layout, as a visual example.
#'
#' @return `numeric` vector length=2, with the recommended number of plot
#' rows and columns, respectively. It is intended to be used directly
#' in this form: `par("mfrow"=decideMfrow(n=5))`
#'
#' @family jam plot functions
#'
#' @param n `integer` number of plot panels
#' @param method `character` string indicating the type of layout to favor.
#' \describe{
#' \item{"aspect"}{uses the device size and aspect ratio of the plot to try
#' to maintain roughly square plot panels.}
#' \item{"wide"}{tries to keep the columns and rows similar, erring on
#' the side of more columns than rows.}
#' \item{"tall"}{tries to keep the columns and rows similar, erring on
#' the side of more rows than columns.}
#' }
#' @param doTest `logical` whether to provide a visual test. Note that
#' \code{n} is required as the number of plot panels requested.
#' @param ... additional parameters are ignored.
#'
#' @examples
#' # display a test visualization showing 6 panels
#' decideMfrow(n=6, doTest=TRUE);
#'
#' # a manual demonstration creating 6 panels
#' n <- 6;
#' par(mfrow=decideMfrow(n));
#' for(i in seq_len(n)){
#' nullPlot(plotAreaTitle=paste("Plot", i));
#' }
#'
#' @export
decideMfrow <- function
(n,
method=c("aspect", "wide", "tall"),
doTest=FALSE,
...)
{
## Purpose is to decide how to arrange plots so that panels are roughly
## square.
dinAspect <- getPlotAspect(type="device");
n1 <- (sqrt(n/dinAspect));
n2 <- (sqrt(n*dinAspect));
n1diff <- abs(round(n1) - n1);
n2diff <- abs(round(n2) - n2);
if (n1diff < n2diff) {
n1 <- round(n1);
n2 <- ceiling(n/n1);
} else {
n2 <- round(n2);
n1 <- ceiling(n/n2);
}
## Optionally provide a visual test
if (doTest) {
oPar <- par(no.readonly=TRUE);
on.exit(par(oPar));
par("mfrow"=c(n1, n2));
for(i in seq_len(n)){
nullPlot(plotAreaTitle=paste("Plot", i));
}
}
c(n1, n2);
}
#' Get aspect ratio for coordinates, plot, or device
#'
#' Get aspect ratio for coordinates, plot, or device
#'
#' @return `numeric` plot aspect ratio for a plot device, of the requested
#' type, see the `type` argument.
#'
#' @family jam plot functions
#'
#' @param type `character` type of aspect ratio to calculate.
#' \describe{
#' \item{"coords"}{calculates plot coordinate aspect ratio, which
#' is helpful for creating proper circular shapes, for example,
#' where the x-axis and y-axis ranges are very different. Note
#' that this calculation does also correct for margin sizes.}
#' \item{"plot"}{calculates plot aspect ratio, based upon the
#' actual size of the plot, independent of the numeric coordinate
#' range of the plot. This aspect ratio reflects the relative
#' visual height and width of the plot area, ignoring margins.}
#' \item{"device"}{calculates plot aspect ratio, based upon the
#' complete graphical device, i.e. the full space including all
#' panels, margins, and plot areas.}
#' }
#' @param parUsr,parPin,parDin `numeric` values equivalent to their
#' respective `par()` output, from `par("usr")`,
#' `par("pin")`, and `par("din")`. Values can be
#' supplied directly, which among other things, prevents opening a
#' graphical device if one is not already opened. Any call to
#' `par()` will otherwise cause a graphic device to be opened,
#' which may not be desired on a headless R server.
#' @param ... additional parameters are ignored.
#'
#' @examples
#' par("mfrow"=c(2,4));
#' for (i in 1:8) {
#' nullPlot(plotAreaTitle=paste("Plot", i), xlim=c(1,100), ylim=c(1,10),
#' doMargins=FALSE);
#' axis(1, las=2);
#' axis(2, las=2);
#' }
#' getPlotAspect("coords");
#' getPlotAspect("plot");
#' getPlotAspect("device");
#'
#' @export
getPlotAspect <- function
(type=c("coords", "plot", "device"),
parUsr=par("usr"),
parPin=par("pin"),
parDin=par("din"),
...)
{
## Purpose is to get the plot aspect ratio, given an open
## plot window, taking into account both the plot region
## and the size of the displayed plot device.
## The end result is a ratio of the x-axis to y-axis
## actual visual width.
##
## type=="coords" uses the x- and y-axis coordinates, along with
## the axis ranges, and plot window size, to determine the coordinate
## aspect ratio. This ratio is useful when creating a perfect square.
##
## type=="plot" uses the plot dimensions in inches, to calculate the
## aspect ratio of the plot pane itself.
##
## type=="device" uses only the device dimensions to determine the
## aspect ratio of the device itself, independent of the size of any
## plot panels inside the device.
type <- match.arg(type);
parDinAspect <- parDin[1] / parDin[2];
if (type %in% "device") {
return(parDinAspect);
}
parPinAspect <- parPin[1] / parPin[2];
if (type %in% "plot") {
return(parPinAspect);
}
plotWidth <- diff(parUsr[1:2]);
plotHeight <- diff(parUsr[3:4]);
## plot device aspect ratio as width:height
plotUsrAspect <- plotHeight / plotWidth;
plotAspect <- plotUsrAspect * parPinAspect;
plotAspect;
}
#' frequency of entries, ordered by frequency
#'
#' frequency of entries, ordered by frequency
#'
#' This function mimics output from `table()` with two key
#' differences. It sorts the results by decreasing frequency, and optionally
#' filters results for a minimum frequency. It is effective when checking
#' for duplicate values, and ordering them by the number of occurrences.
#'
#' This function is useful when working with large vectors of gene
#' identifiers, where it is not always obvious whether genes are replicated
#' in a particular technological assay. Transcript microarrays for example,
#' can contain many replicated genes, but often only a handful of genes are
#' highly replicated, while the rest are present only once or twice on the
#' array.
#'
#' @return `integer` vector of counts, named by the unique input
#' values in `x`.
#'
#' @family jam string functions
#'
#' @param x `character`, `numeric`, `factor` vector input to use when
#' calculating frequencies.
#' @param doSort `logical` whether to sort results decreasing by frequency.
#' @param minCount optional `integer` minimum frequency, any results with
#' fewer counts observed will be omitted from results.
#' @param maxCount optional `integer` maximum frequency for returned results.
#' @param nameSortFunc `function` used to sort results after sorting by
#' frequency. For example, one might use `mixedSort()`. If
#' `nameSortFunc=NULL` then no name sort will be applied.
#' @param ... additional parameters are ignored.
#'
#' @examples
#' testVector <- rep(c("one", "two", "three", "four"), c(1:4));
#' tcount(testVector);
#' tcount(testVector, minCount=2);
#'
#' @export
tcount <- function
(x,
minCount=NULL,
doSort=TRUE,
maxCount=NULL,
nameSortFunc=sort,
...)
{
## Purpose is similar to table(), except this is just a quick way to return counts of each element,
## sorted by the counts in decreasing order. tcount(x)[1] is a quick way to see if any element is
## present more than once.
##
## minCount will filter results to those having at least that high a count
## Note we detect factor class in reverse, since ordered factors have two class values
## and would otherwise fail to be detected if we use class(x) %in% "factor"
if (c("factor") %in% class(x)) {
x <- as.character(x);
}
#x1 <- tapply(x, x, length);
x1 <- table(x);
x1 <- nameVector(as.vector(x1), names(x1), makeNamesFunc=c);
## Filter before sort, for potential speed gain
if (!is.null(minCount)) {
x1 <- x1[x1 >= minCount];
}
if (!is.null(maxCount)) {
x1 <- x1[x1 <= maxCount];
}
if (doSort) {
if (!is.null(nameSortFunc)) {
x1 <- x1[match(nameSortFunc(names(x1)), names(x1))];
}
x1 <- sort(x1, decreasing=TRUE);
}
return(x1);
}
#' frequency of entries, ordered by frequency, minimum count 2
#'
#' frequency of entries, ordered by frequency, minimum count 2
#'
#' This function is a simple customization of `tcount()`
#' with `minCount=2` so it only reports frequencies of `2` or higher.
#'
#' @return `integer` vector of counts, named by the unique input
#' values in `x`, by default limited to entries with frequency
#' `2` or higher.
#'
#' @family jam string functions
#'
#' @rdname tcount
#'
#' @export
tcount2 <- function
(x,
minCount=2,
doSort=TRUE,
maxCount=NULL,
nameSortFunc=sort,
...)
{
x1 <- tcount(x=x,
minCount=minCount,
doSort=doSort,
maxCount=maxCount,
nameSortFunc=nameSortFunc,
...)
return(x1)
}
#' vectorized make_styles for crayon output
#'
#' vectorized make_styles for crayon output
#'
#' This function is essentially a vectorized version of
#' `crayon::make_style()` in order to style a vector of
#' character strings with a vector of foreground and background styles.
#'
#' @return
#' `character` vector with the same length as `text` input vector, where
#' entries are surrounded by the relevant encoding consistent with
#' the `style` defined at input. In short, a character vector as input,
#' a colorized character vector as output.
#'
#' @family jam practical functions
#' @family jam color functions
#'
#' @param style `character` vector of one or more styles. When `NULL` or `NA`,
#' no style is applied, except when `bg_style` is supplied
#' and is neither `NA` nor `NULL`, in which case entries with
#' a `bg_style` and no `style` will use `setTextContrastColor()`
#' to define a contrasting `style`.
#' @param text `character` vector (or coerced to `character`) of one or
#' more values,.
#' @param bg `logical` indicating whether the `style` should be
#' applied to the background instead of foreground. This argument
#' is ignored when `bg_style` is supplied.
#' @param bg_style `NULL` or a `character` vector of one or more background
#' styles. When this argument is not NULL, it applies both the foreground
#' `style` and background `bg_style` together, and therefore ignores
#' `Crange` and `Lrange` settings.
#' @param colors `integer` number of colors allowed for console output.
#' @param satCutoff `numeric` cutoff for color saturation, below which a color
#' is considered grey and the ANSI greyscale color set is used.
#' @param Cgrey `numeric` chroma (C) value, which defines grey colors at or
#' below this chroma. Any colors at or below the grey cutoff will have
#' use ANSI greyscale coloring. To disable, set `Cgrey=-1`.
#' @param lightMode `logical` indicating whether the background color
#' is light (TRUE is bright), or dark (FALSE is dark.) By default
#' it calls `checkLightMode()` which queries `getOption("lightMode")`.
#' @param Crange `numeric` range of chroma values, ranging
#' between 0 and 100. When NULL, default values will be
#' assigned to Crange. When supplied, range(Crange) is used.
#' @param Lrange `numeric` range of luminance values, ranging
#' between 0 and 100. When NULL, default values will be
#' assigned to Lrange. When supplied, range(Lrange) is used.
#' @param adjustRgb `numeric` value adjustment used during the conversion of
#' RGB colors to ANSI colors, which is inherently lossy. If not defined,
#' it uses the default returned by `setCLranges()` which itself uses
#' `getOption("jam.adjustRgb")` with default=0. In order to boost
#' color contrast, an alternate value of -0.1 is suggested.
#' @param adjustPower `numeric` adjustment power factor
#' @param fixYellow `logical` indicating whether to "fix" the darkening of
#' yellow, which otherwise turns to green. Instead, since JAM can,
#' JAM will make the yellow slightly more golden before darkening. This
#' change only affects color hues between 80 and 90. This argument is
#' passed to `applyCLrange()`.
#' @param colorTransparent `character` color used to substitute for
#' "transparent" which a valid R color, but not a valid color for
#' the crayon package.
#' @param alphaPower `numeric` value, used to adjust the RGB values for alpha
#' values less than 255, by raising the ratio to 1/alphaPower, which takes
#' the ratio of square roots. alphaPower=100 for minimal adjustment.
#' @param setOptions `character` or `logical` whether to update
#' `Crange` and `Lrange` options during the subsequent call to
#' `setCLranges()`. By default,
#' * `"ifnull"` will update only options which were previously `NULL`;
#' * `"FALSE"` prevents modifying the global options;
#' * `"TRUE"` will update these options with the current values.
#' @param verbose `logical` indicating whether to print verbose output
#' @param ... additional parameters are ignored
#'
#'
#' @export
make_styles <- function
(style=NULL,
text,
bg=FALSE,
bg_style=NULL,
grey=FALSE,
colors=num_colors(),
Cgrey=getOption("jam.Cgrey", 5),
lightMode=NULL,
Crange=getOption("jam.Crange"),
Lrange=getOption("jam.Lrange"),
adjustRgb=getOption("jam.adjustRgb"),
adjustPower=1.5,
fixYellow=TRUE,
colorTransparent="grey45",
alphaPower=2,
setOptions=c("ifnull","FALSE","TRUE"),
verbose=FALSE,
...)
{
## Purpose is to wrapper make_style for a vector of styles
## and text. By default make_style() only accepts one style
## and returns a function, not the text value
##
## style is repeated to match the length of the vector text,
## however text is not repeated to match the length of style.
##
## This function also accepts NA as input, returning the input
## text with no modification, intended for vectorized operations.
##
## checkSat=TRUE will check whether the color is extremely low saturation,
## in which case, grey=TRUE is automatically set.
##
## adjustRgb=TRUE will pre-adjust the RGB values, which in crayon do
## a conversion to the ANSI 6-bit color scale, which tends to round
## values up, losing subtle differences because colors tend to be too bright.
##
## Lastly, if text has names, they are preserved in the output.
##
## subTransparent is used to replace the valid R color "transparent"
## with something compatible with crayon::make_style()
## subTransparent=NA will perform no styling on transparent entries,
## thus using the ANSI default text color
##
## alphaPower=2 is used to adjust the RGB values for alpha values
## less than 255, by raising the ratio to 1/alphaPower, which takes the
## ratio of square roots. alphaPower=100 for minimal adjustment.
##
if (length(text) == 0) {
return(text);
}
if (!suppressWarnings(suppressPackageStartupMessages(require(crayon)))) {
## If crayon is not available, return text without style. So sad.
return(text);
}
if (length(setOptions) == 0) {
setOptions <- "ifnull";
} else {
setOptions <- as.character(setOptions);
}
if (length(Cgrey) == 0) {
Cgrey <- -1;
}
if (length(colorTransparent) == 0) {
colorTransparent <- NA;
}
## Determine Crange, Lrange, adjustRgb
CLranges <- setCLranges(lightMode=lightMode,
Crange=Crange,
Lrange=Lrange,
Cgrey=Cgrey,
setOptions=setOptions,
...);
if (length(adjustRgb) == 0) {
adjustRgb <- CLranges$adjustRgb;
}
Crange <- CLranges$Crange;
Lrange <- CLranges$Lrange;
if (verbose) {
print(paste0("make_styles(): ",
"Crange:",
paste(Crange, collapse=","),
", Lrange:",
paste(Lrange, collapse=","),
", adjustRgb:",
adjustRgb,
", fixYellow:",
fixYellow));
}
if (length(fixYellow) == 0) {
fixYellow <- FALSE;
}
fixYellow <- rep(fixYellow, length.out=length(text));
## Process style
if (length(style) > 0 && igrepHas("matrix", class(style))) {
if (verbose) {
print(paste0("make_styles(): ",
"Handling argument style as matrix."))
}
if (!all(c("red", "green", "blue") %in% rownames(style))) {
stop("When style is a matrix it must contain rownames 'red', 'green', and 'blue'.");
}
styleNA <- (is.na(style["red",]) |
is.na(style["green",]) |
is.na(style["blue",]));
## Convert to color vector to apply CL range, then back to rgb
styleV <- rgb2col(style);
styleV <- rep(styleV, length.out=length(text));
styleNA <- rep(styleNA, length.out=length(text));
} else {
if (verbose) {
print(paste0("make_styles(): ",
"Handling argument style as vector."))
}
if (length(style) == 0 || length(unlist(style)) == 0) {
style <- NA;
}
style <- rep(style, length.out=length(text));
styleNA <- is.na(style);
styleV <- style;
}
## Process bg_style
if (length(bg_style) > 0 && igrepHas("matrix", class(bg_style))) {
if (verbose) {
print(paste0("make_styles(): ",
"Handling argument bg_style as matrix."))
}
if (!all(c("red", "green", "blue") %in% rownames(bg_style))) {
stop("When bg_style is a matrix it must contain rownames 'red', 'green', and 'blue'.");
}
styleNA <- (is.na(bg_style["red",]) |
is.na(bg_style["green",]) |
is.na(bg_style["blue",]));
## Convert to color vector to apply CL range, then back to rgb
bg_styleV <- rgb2col(bg_style);
bg_styleV <- rep(bg_styleV, length.out=length(text));
bg_styleNA <- rep(bg_styleNA, length.out=length(text));
} else {
if (verbose) {
print(paste0("make_styles(): ",
"Handling argument bg_style as vector."))
}
if (length(bg_style) == 0 || length(unlist(bg_style)) == 0) {
bg_style <- NA;
}
bg_style <- rep(bg_style, length.out=length(text));
bg_styleNA <- is.na(bg_style);
bg_styleV <- bg_style;
}
if (length(bg) == 0) {
bg <- FALSE;
}
bg <- rmNA(naValue=FALSE,
rep(bg, length.out=length(text)));
## Optionally apply fixYellow() to bg
if (any(fixYellow & !bg_styleNA)) {
## fixYellow
bg_style[!bg_styleNA & fixYellow] <- fixYellow(bg_style[!bg_styleNA & fixYellow],
...);
}
if (verbose) {
print(paste0("styleV (before):", cPaste(styleV)));
print(styleV);
print(paste0("bg_styleV (before):", cPaste(bg_styleV)));
}
## Apply Crange, Lrange only when bg_style is NA
if (any(bg_styleNA & !styleNA)) {
if (verbose) {
print(paste0("make_styles(): ",
"applyCLrange()"));
}
styleV[bg_styleNA & !styleNA] <- applyCLrange(styleV[bg_styleNA & !styleNA],
Lrange=Lrange,
Crange=Crange,
Cgrey=Cgrey,
fixYellow=fixYellow,
verbose=verbose,
...);
}
## Check for any colors too close to white, they
## cause a bug where any foreground color is always
## white if one color is ANSI white \033[38;5;255
## The workaround is to make them 98% white
style_white <- grepl("^#F[89ABCDEF]F[89ABCDEF]F[89ABCDEF]|^white$", styleV);
if (any(style_white)) {
if (verbose) {
print(paste0("make_styles(): ",
"fixing style_white"));
}
styleV[style_white] <- "#F7F7F7";
}
## Optionally apply fixYellow() to styleV
if (any(fixYellow & !styleNA)) {
## fixYellow
styleV[!styleNA & fixYellow] <- fixYellow(styleV[!styleNA & fixYellow],
...);
}
if (verbose) {
print(paste0("styleV (after):",
paste(styleV, collapse=",")));
print(paste0("bg_styleV (after):",
paste(bg_styleV, collapse=",")));
}
## Convert to rgb
style <- col2rgb(styleV, alpha=TRUE);
if (any(styleNA)) {
style[,styleNA] <- NA;
}
bg_style <- col2rgb(bg_styleV, alpha=TRUE);
if (any(bg_styleNA)) {
bg_style[,bg_styleNA] <- NA;
}
if (verbose) {
print(paste0("make_styles(): ",
"style:"));
print(style);
print(paste0("make_styles(): ",
"bg_style:"));
print(bg_style);
}
## Apply alpha and check for transparent colors
if ("alpha" %in% rownames(style) &&
any(rmNA(style["alpha",], naValue=255) < 255)) {
if (verbose) {
print(paste0("make_styles(): ",
"applying alpha."));
}
alphaFactor <- (style["alpha",])^(1/alphaPower)/(255)^(1/alphaPower);
style[c("red","green","blue"),] <- style[c("red","green","blue"),] *
rep(alphaFactor, each=3);
isTransparent <- (style["alpha",] == 0);
} else {
isTransparent <- rep(FALSE, length.out=length(text));
}
## Remove transparency
style <- style[c("red","green","blue"),,drop=FALSE];
## Adjust RGB
if (adjustRgb != 0 && any(!styleNA)) {
if (verbose) {
print(paste0("make_styles(): ",
"applying adjustRgb"));
}
if (!is.na(adjustPower) && !is.null(adjustPower)) {
## This method uses square root transform
style1 <- round((style^adjustPower)/(255^adjustPower)*6 + adjustRgb);
} else {
## This method shifts color brightness down slightly
style1 <- round(style/255*6 + adjustRgb);
}
style[!is.na(style)] <- style1[!is.na(style)] * 255/6;
style[!is.na(style) & style < 1] <- 1;
style[!is.na(style) & style > 255] <- 255;
if (verbose) {
print(paste0("make_styles(): ",
"style (post-adjustRgb):"));
print(style);
}
}
## Check color saturation for greyscale colors
if (any(!styleNA)) {
if (verbose) {
print(paste0("make_styles(): ",
"determining grey colors"));
}
Cvals <- rep(100, length(styleV));
Cvals[!styleNA] <- col2hcl(styleV[!styleNA])["C",];
isCgrey <- (Cvals <= Cgrey);
} else {
isCgrey <- rep(FALSE, length.out=length(text));
}
## Apply each style to each text entry
iVals <- sapply(seq_along(text), function(i){
iText <- text[i];
iStyle <- style[,i,drop=FALSE];
ibgStyle <- bg_style[,i,drop=FALSE];
iGrey <- isCgrey[i];
if (styleNA[i] && bg_styleNA[i]) {
if (verbose) {
print(paste0("make_styles(): ",
"No style applied to text:",
iText));
}
iText;
} else if (styleNA[i] && !bg_styleNA[i]) {
## Combine bg with contrasting fg color
bg_contrast <- setTextContrastColor(rgb2col(ibgStyle),
useGrey=5);
if (verbose) {
print(paste0("make_styles(): ",
"bg style and contrasting fg style applied to text:",
iText));
print(paste0("iText:", iText,
", colors:", colors,
", bg_contrast:", bg_contrast,
", rgb2col(ibgStyle):", rgb2col(ibgStyle)));
}
make_style(rgb2col(ibgStyle),
bg=TRUE,
colors=colors)(
make_style(bg_contrast,
bg=FALSE,
colors=colors)(iText)
);
} else if (!styleNA[i] && !bg_styleNA[i]) {
## Combine bg with fg color
if (verbose) {
print(paste0("make_styles(): ",
"bg style and fg style applied to text:",
iText));
}
make_style(rgb2col(ibgStyle),
bg=TRUE,
colors=colors)(
make_style(rgb2col(iStyle),
bg=FALSE,
colors=colors)(iText)
);
} else {
if (verbose) {
print(paste0("make_styles(): ",
"fg style applied to text:",
iText));
}
if (isTransparent[i]) {
iStyle <- col2rgb(colorTransparent, alpha=TRUE);
}
if (verbose) {
print(paste0("make_styles(): ",
"iStyle:"));
print(iStyle);
print(paste0("make_styles(): ",
"bg:", bg));
print(paste0("make_styles(): ",
"colors:", colors));
print(paste0("make_styles(): ",
"iGrey:", iGrey));
}
make_style(rgb2col(iStyle),
bg=bg[i],
colors=colors,
grey=iGrey)(iText);
}
});
if (length(names(text)) > 0) {
names(iVals) <- names(text);
}
attr(iVals, "color") <- rgb2col(style);
attr(iVals, "bg_color") <- rgb2col(bg_style);
iVals;
}
#' Show R function arguments jam-style
#'
#' Show R function arguments jam-style
#'
#' This function displays R function arguments, organized with one argument
#' per line, and colorized using the `crayon` package if
#' installed.
#'
#' Output is nicely spaced to help visual alignment of argument names
#' and argument values.
#'
#' Output can be filtered by `character` pattern. For example the
#' function `ComplexHeatmap::Heatmap()` is amazing, and offers numerous
#' arguments. To find arguments relevant to dendrograms, use `"dend"`:
#'
#' `jargs(ComplexHeatmap::Heatmap, "dend")`
#'
#' NOTE: This function has edge case issues displaying complex function
#' argument values such as nested lists and custom functions.
#' In that case the argument name is printed as usual, and the argument value
#' is displayed as a partial snippet of the default argument value.
#'
#' Generic functions very often contain no useful parameters,
#' making it difficult to discover required
#' parameters without reading the function documentation from the proper
#' dispatched function and calling package. In that case,
#' try using `jargs(functionname.default)` for example compare:
#'
#' `jargs(barplot)`
#'
#' to:
#'
#' `jargs(barplot.default)`
#'
#' @family jam practical functions
#'
#' @param x `function` or character name of a function.
#' @param grepString `NULL`, `logical`, or `character` grep regular expression
#' pattern used to filter function arguments by name. Very useful to
#' search a function for arguments with a substring `"row"`.
#' * If `logical`, it is assumed to be sortVars, and indicates whether
#' to sort the parameter names.
#' * if `character` it will subset the function arguments by name matching
#' this regular expression pattern.
#' @param sortVars `logical` whether to sort the function parameter names.
#' * `sortVars=FALSE` returns arguments in the order they appear in the
#' function definition.
#' * `sortVars=TRUE` returns arguments sorted alphabetically.
#' @param asList `logical` whether to display one entry per line (default), or
#' display results as a `data.frame`.
#' @param useColor `logical` whether to display results in color, if the crayon
#' package is available, and terminal console is capable.
#' @param lightMode `logical` or `NULL`, indicating whether the text background
#' color is light, thus imposing a maximum brightness for colors displayed.
#' It use lightMode if defined by the function caller, otherwise it will
#' use `getOption("jam.lightMode")` if defined, lastly it will attempt to
#' detect whether running inside Rstudio by checking the environment variable
#' "RSTUDIO", and if so it will assume lightMode==TRUE.
#' @param Crange `numeric` range of chroma values, ranging
#' between 0 and 100. When NULL, default values will be
#' assigned to Crange by `setCLranges()`.
#' @param Lrange `numeric` range of luminance values, ranging
#' between 0 and 100. When NULL, default values will be
#' assigned to Lrange by `setCLranges()`.
#' @param adjustRgb `numeric` value adjustment used during the conversion of
#' RGB colors to ANSI colors, which is inherently lossy. If not defined,
#' it uses the default returned by `setCLranges()` which itself uses
#' \code{getOption("jam.adjustRgb")} with default=0. In order to boost
#' color contrast, an alternate value of -0.1 is suggested.
#' @param useCollapseBase `character` string used to combine multiple parameter
#' values.
#' @param verbose `logical` whether to print verbose output.
#' @param debug `integer` value, greater than 0 will cause debug-type verbose
#' output, useful because parameters are hard!
#'
#' @examples
#' args(jargs)
#' jargs(jargs)
#'
#' # retrieve parameters involving notes from imageByColors
#' jargs(imageByColors, "note")
#'
#' @export
jargs <- function
(x,
grepString=NULL,
sortVars=FALSE,
asList=TRUE,
useColor=TRUE,
lightMode=NULL,
Crange=getOption("jam.Crange"),
Lrange=getOption("jam.Lrange"),
adjustRgb=getOption("jam.adjustRgb"),
useCollapseBase=", ",
verbose=FALSE,
debug=0,
...)
{
## Purpose is to clean up the args() output to my personal preferences
##
## grepString is used to subset the arguments by name
##
## sortVars=TRUE by default, sorts the argument names, except that
## the '...' argument is placed last for legibility
##
## asList=TRUE uses display similar to default args()
## asList=FALSE uses data.frame colsHead() display
##
## useColor=TRUE because why not, right?
## Check lightMode, whether the background color is light or not
if (length(lightMode) == 0 && length(Crange) > 0 && length(Lrange) > 0) {
# use them as-is
if (length(adjustRgb) == 0) {
adjustRgb <- CLranges$adjustRgb;
}
} else {
CLranges <- setCLranges(lightMode=lightMode,
Crange=Crange,
Lrange=Lrange,
adjustRgb=adjustRgb,
...);
adjustRgb <- CLranges$adjustRgb;
Crange <- CLranges$Crange;
Lrange <- CLranges$Lrange;
}
if (useColor) {
if (suppressWarnings(suppressPackageStartupMessages(require(crayon)))) {
useCrayon <- TRUE;
} else {
if (verbose) {
printDebug("jargs(): ",
"Turned color off since the ",
"crayon",
" package is not available.");
}
useColor <- FALSE;
}
} else {
useCrayon <- FALSE;
}
if (length(grepString) > 0 && grepString %in% c(FALSE)) {
grepString <- setdiff(grepString, FALSE);
sortVars <- FALSE;
}
## Get function arguments
argsText <- formals(x);
if (length(grepString) > 0) {
argsText <- argsText[vigrep(grepString, names(argsText))];
if (length(argsText) == 0) {
if (verbose) {
printDebug("jargs(): ",
"No arguments matched the grepString.",
fgText="yellow");
}
invisible(NULL);
}
}
if (length(argsText) > 0 && asList) {
varLen <- nameVector(nchar(names(argsText)), names(argsText));
indent <- paste(rep(" ", max(varLen)+1), collapse="");
x1 <- as.vector(gsub("=$", "", sapply(names(argsText), function(i){
if (verbose) {
printDebug("jargs(): ",
"i:",
i);
}
col1 <- "mediumpurple2";
col2 <- "mediumaquamarine";
deText <- handleArgsText(argsText[[i]],
col1=col1,
col2=col2,
indent=indent,
lightMode=NULL,
adjustRgb=adjustRgb,
Crange=Crange,
Lrange=Lrange,
useCollapseBase=useCollapseBase,
useColor=useCrayon,
debug=debug,
verbose=verbose);
aText <- paste(i, paste(deText, collapse=" "), sep=" = ");
aText;
})));
names(x1) <- names(argsText);
if (sortVars && length(x1) > 1) {
x2 <- mixedSort(x1);
} else {
x2 <- x1;
}
varLen <- varLen[names(x2)];
varLen1 <- sapply(x2, function(i){
nchar(strsplit(i, "=")[[1]])[1]
});
x3 <- sapply(1:length(x2), function(i){
paste0(
c(rep(" ", max(varLen) - varLen[i]),
x2[i]),
collapse="");
});
cat(paste(x3, collapse=",\n"));
cat("\n");
} else {
argsTable <- do.call(cbind, lapply(argsText, deparse));
if (sortVars) {
argsTable <- argsTable[,mixedSort(colnames(argsTable)), drop=FALSE];
}
print(argsTable);
invisible(argsTable);
}
}
#' Handle function arguments as text
#'
#' Handles a list or list of lists, converting to human-readable text format
#'
#' This function is a rare non-exported function intended to be called by
#' `jargs()`, but separated in order to help isolate the logical
#' steps required.
#'
#' @family jam practical functions
#'
#' @inherit jargs
#' @param argTextA object passed by `jargs()` when iteratively parsing
#' function argument values.
#' @param name `character` name of the argument.
#' @param col1,col2,colT,colF,colNULL `character` colors used as defaults
#' for first and second arguments, TRUE, FALSE, NULL, respectively.
#' @param indent `character` string used as a prefix in output to help
#' apply text indent.
#' @param useCollapseList `character` string inserted between multiple values
#' to split list entries across multiple lines.
#' @param useCollapseBase `character` string used to separate multiple
#' values in a vector which is not split across multiple lines.
#' @param level `integer` indicating the level of depth in iterative parsing.
#'
handleArgsText <- function
(argTextA,
name="",
col1="mediumpurple2",
col2="mediumaquamarine",
colT="dodgerblue3",
colF="red1",
colNULL="grey60",
lightMode=NULL,
Crange=getOption("jam.Crange"),
Lrange=getOption("jam.Lrange"),
adjustRgb=getOption("jam.adjustRgb"),
indent="",
useCollapseList=",\n ",
useCollapseBase=", ",
level=1,
debug=0,
useColor=TRUE,
verbose=FALSE,
...)
{
## Purpose is to take input in the form of formals(functionname)
## and return a formatted string suitable for printing to the R
## console.
##
#argTextA <- argsTextA[[i]];
if (level == 20) {
useCollapseBase <- useCollapseList;
}
if ("name" %in% class(argTextA)) {
deTextA <- deparse(argTextA);
} else {
deTextA <- deparse(argTextA[[1]]);
}
# version 0.0.88.900: assume input already resolved Crange,Lrange,adjustRgb
# ## Check lightMode, whether the background color is light or not
# CLranges <- setCLranges(lightMode=lightMode,
# Crange=Crange,
# Lrange=Lrange,
# ...);
# if (length(adjustRgb) == 0) {
# adjustRgb <- CLranges$adjustRgb;
# }
# Lrange <- CLranges$Lrange;
# Crange <- CLranges$Crange;
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"class(argTextA):",
class(argTextA),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
if (debug > 0 && verbose) {
printDebug(indent, "", "=== handleArgsText():\n ",
"name:\n ",
name, "\n ", "",
"as.character(argTextA):\n ",
as.character(argTextA), "\n ", "",
"deTextA:\n ",
deTextA,
sep="\n ",
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
if (any(c("pairlist","call") %in% class(argTextA))) {
##
## Class is "call"
##
## Multi-value entry
whichMid <- tail(seq_along(argTextA), -1);
whichEnds <- setdiff(seq_along(argTextA), whichMid);
firstArg <- deparse(argTextA[[whichEnds]]);
firstArgName <- names(argTextA)[whichEnds];
##
if (firstArg %in% "function") {
##
##################################
## Handle functions
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"firstArg is 'function'",
", whichEnds=", whichEnds,
", whichMid=", whichMid,
", argTextA:",
fgText=c("mediumaquamarine","yellow"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
print(argTextA);
for (i1 in seq_along(argTextA)) {
printDebug(indent, "", "handleArgsText(): ",
"argTextA[[", i1, "]]",
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
print(argTextA[[i1]]);
}
}
## Parse functions differently than other entries
whichMid <- head(tail(seq_along(argTextA), -1), -1);
whichEnds <- setdiff(seq_along(argTextA), whichMid);
fnArgs <- argTextA[[head(whichMid,1)]];
fnArgsNames <- names(fnArgs);
fnArgsText <- handleArgsText(fnArgs,
name="",
col1=col1,
col2=col2,
indent=paste0(indent, " "),
useCollapseBase=useCollapseBase,
Crange=Crange,
Lrange=Lrange,
adjustRgb=adjustRgb,
level=level+1,
useColor=useColor,
debug=1,
verbose=verbose);
fnBody <- deparse(argTextA[[3]]);
if (length(fnBody) > 1) {
fnBody[2:length(fnBody)] <- paste0(indent,
" ",
fnBody[2:length(fnBody)]);
}
fnBodyText <- paste0(fnBody, collapse="\n");
if (useColor) {
deTextA <- paste0(
make_styles(
text=c("function(",
fnArgsText,
")",
fnBodyText),
style=c(col1, col2),
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE"),
collapse=" ");
} else {
deTextA <- paste(
"function(",
fnArgsText,
")",
fnBodyText,
collapse=" ");
}
} else {
##
##################################
## Handle non-functions
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"pairlist firstArg:",
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
print(firstArg);
printDebug(indent, "", "handleArgsText(): ",
"name: '",
name,
"'",
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
printDebug(indent, "", "handleArgsText(): ",
"whichMid:",
whichMid,
", length(whichMid):",
length(whichMid),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
if ("pairlist" %in% class(argTextA) &&
length(whichMid) > 0 &&
all(isColor(as.character(argTextA[whichMid])))) {
############################################
## Handle pairlist, all values are colors
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"'c1' && all(isColor)",
", calling handleArgsText()",
fgText=c("orange", "aquamarine1"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
argTextA[whichMid] <- sapply(whichMid, function(j1){
j <- argTextA[[j1]];
jName <- names(argTextA)[j1];
handleArgsText(j,
name=jName,
col1=as.character(j),
col2=as.character(j),
indent=paste0(indent, " "),
useCollapseBase=useCollapseBase,
adjustRgb=adjustRgb,
Crange=Crange,
Lrange=Lrange,
level=level+1,
useColor=useColor,
debug=debug,
verbose=verbose);
});
} else if ("pairlist" %in% class(argTextA) &&
length(whichMid) > 0 &&
all(is.numeric(argTextA[whichMid]))) {
############################################
## Handle pairlist, all values are numeric
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"'c1' && all(is.numeric)",
", calling handleArgsText()",
fgText=c("orange", "aquamarine1"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
if (suppressWarnings(suppressPackageStartupMessages(require(colorjam)))) {
arg_colors <- colorjam::vals2colorLevels(argTextA[whichMid],
divergent=TRUE,
lens=50,
col=getColorRamp("RdBu_r", trimRamp=c(3, 3)));
} else {
arg_colors <- rep(c("skyblue","mediumslateblue"),
length.out=length(argTextA[whichMid]));
}
names(arg_colors) <- as.character(whichMid);
argTextA[whichMid] <- sapply(whichMid, function(j1){
j <- argTextA[[j1]];
jName <- names(argTextA)[j1];
handleArgsText(j,
name=jName,
col1=arg_colors[as.character(j1)],
col2=arg_colors[as.character(j1)],
indent=paste0(indent, " "),
useCollapseBase=useCollapseBase,
adjustRgb=adjustRgb,
Crange=Crange,
Lrange=Lrange,
level=level+1,
useColor=useColor,
debug=debug,
verbose=verbose);
});
} else {
############################################
## Handle pairlist, values are not colors
if (firstArg %in% "list") {
useCollapseBase <- useCollapseList;
} else {
useCollapseBase <- ", ";
}
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"firstArg is not 'function', !all(isColor)",
", calling handleArgsText()",
fgText=c("lightgreen","orange"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
if ("pairlist" %in% class(argTextA)) {
whichMid <- seq_along(argTextA);
whichEnds <- setdiff(whichEnds, whichMid);
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"-- pairlist, setting whichMid <- seq_along(argTextA):",
whichMid,
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
}
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"Iterating argTextA[whichMid], ",
"whichMid=", whichMid,
", whichEnds=", whichEnds,
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
argTextA[whichMid] <- sapply(whichMid, function(j1){
jName <- names(argTextA)[j1];
if (debug > 0 && verbose) {
printDebug(indent, "calling handleArgsText(): ",
"whichMid[j1], j1=",
j1,
", jName:",
jName,
fgText=c("lightsalmon", "yellow"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
if ("pairlist" %in% class(argTextA)) {
j <- argTextA[j1];
} else {
j <- argTextA[[j1]];
}
handleArgsText(argTextA=j,
name=jName,
col1=col2,
col2=col1,
indent=paste0(indent, " "),
useCollapseBase=useCollapseBase,
level=level+1,
useColor=useColor,
adjustRgb=adjustRgb,
Crange=Crange,
Lrange=Lrange,
debug=debug,
verbose=verbose);
});
}
if (length(whichEnds) > 0) {
if (useColor) {
argTextA[whichEnds] <- make_styles(text=firstArg,
style=col1,
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE");
} else {
argTextA[whichEnds] <- firstArg;
}
}
if (igrepHas("[a-zA-Z]", firstArg)) {
## Format: function("value1", "value2")
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"Format: function('value1', 'value2')",
", name:", name,
fgText=c("purple1", "yellow"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
printDebug(indent, "", "handleArgsText(): ",
"Collapsing params into multiple lines with ",
"useCollapseBase",
" then some indention",
fgText=c("lightblue3","orange1"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
if (igrepHas("[\n]", useCollapseBase)) {
useCollapse <- paste0(useCollapseBase, indent);
} else {
useCollapse <- useCollapseBase;
}
deTextA <- paste0(
ifelse(nchar(name)>0,
paste0(name, "="),
""),
argTextA[whichEnds],
"(",
paste(argTextA[whichMid], collapse=useCollapse),
")",
collapse=" ");
} else if (length(argTextA[whichMid]) == 2) {
## Format: "value1" || "value2"
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"Format: 'value1' || 'value2'",
", name:",
name,
fgText=c("lightpink1","lightsalmon1"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
printDebug("whichEnds:", whichEnds,
", whichMid:", whichMid);
for (i1 in seq_along(argTextA)) {
printDebug("argTextA[[", i1, "]]");
print(argTextA[[i1]]);
}
}
if ("[" %in% as.character(argTextA[whichEnds]) ||
(useColor &&
"[" %in% crayon::strip_style(argTextA[whichEnds]))) {
## Special case where "[" must also be closed
deTextA <- paste(argTextA[head(whichMid,1)],
argTextA[whichEnds],
argTextA[tail(whichMid,1)],
gsub("([^\033]|^)[[]",
"\\1]",
argTextA[whichEnds]),
sep=" ");
} else {
deTextA <- paste(argTextA[head(whichMid,1)],
argTextA[whichEnds],
argTextA[tail(whichMid,1)],
sep=" ");
}
} else {
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"Format: generic",
", name:",
name,
fgText=c("lightskyblue","lightpink"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
if (useColor && length(name) > 0) {
argTextA <- make_styles(text=paste0(name,
ifelse(nchar(name)>0,"=",""),
as.character(argTextA)),
style=col2,
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE");
} else {
argTextA <- paste0(name,
ifelse(nchar(name)>0,"=",""),
as.character(argTextA),
collapse="");
}
deTextA <- argTextA;
}
}
} else if (length(deTextA) > 1 &&
igrepHas("^[a-zA-Z0-9]+[(]", deTextA[1])) {
##
## Class is not "call" but is multi-entry
##
## !class(argTextA) %in% "call"
## Multi-value entry
whichMid <- head(tail(seq_along(deTextA), -1), -1);
whichEnds <- setdiff(seq_along(deTextA), whichMid);
if (useColor) {
deTextA[whichMid] <- make_styles(
text=as.character(deTextA[whichMid]),
style=col2,
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE");
deTextA[whichEnds] <- make_styles(
text=as.character(deTextA[whichEnds]),
style=col1,
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE");
} else {
deTextA[whichMid] <- as.character(deTextA[whichMid]);
deTextA[whichEnds] <- as.character(deTextA[whichEnds]);
}
aText <- paste(i, paste(deTextA, collapse=" "), sep=" = ");
} else if ("logical" %in% class(argTextA)) {
##
## Class is logical, we colorize TRUE and FALSE
##
if (useColor) {
if (igrepHas("FALSE", deTextA)) {
if (length(name) > 0 && nchar(name) > 0) {
## Value has a name, so print "name=FALSE"
deTextA <- paste0(
make_styles(text=c(name, "=", as.character(deTextA)),
style=c(col1, NA, colF),
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE"),
collapse="");
} else {
## Value has no name, so print "FALSE"
deTextA <- make_styles(
text=as.character(deTextA),
style=colF,
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE");
}
} else {
if (length(name) > 0 && nchar(name) > 0) {
## Value has a name, so print "name=TRUE"
deTextA <- paste0(
make_styles(text=c(name, "=", as.character(deTextA)),
style=colT,
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE"),
collapse="");
} else {
## Value has no name, so print "TRUE"
deTextA <- make_styles(text=as.character(deTextA),
style=colT,
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE");
}
}
} else {
## no colorization of logical string
if (length(name) > 0 && nchar(name) > 0) {
## Value has a name, so print "name=TRUE"
deTextA <- paste0(c(name, "=", as.character(deTextA)),
collapse="");
} else {
## Value has no name, so print "TRUE"
deTextA <- as.character(deTextA);
}
}
} else {
##
## Class is not "call", not "logical"
##
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"class(argTextA):", class(argTextA),
", deTextA (before make_styles):",
deTextA,
fgText=c("lightsteelblue","lightsalmon2"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
if (length(argTextA) > 0 &&
useColor &&
all(isColor(rmNA(naValue="grey35", as.character(argTextA))))) {
##
## Handle parameter values which are all colors, by using those
## colors for the output text color
if (length(name) > 0 && nchar(name) > 0) {
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
" named ", " colored", " parameter",
fgText=c("lightpink2","lightblue3"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
deTextA <- paste0(
make_styles(text=c(name, "=", as.character(deTextA)),
style=c(col1, NA,
rmNA(naValue="grey35", as.character(argTextA))),
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE"),
collapse="");
} else {
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"unnamed ", " colored", " parameter",
fgText=c("lightpink2","lightblue3"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
deTextA <- paste0(
make_styles(text=as.character(deTextA),
style=as.character(argTextA),
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE"),
collapse="");
}
} else {
##
## Parameter values are not colors, so we use default colors here
if (length(name) > 0 && nchar(name) > 0) {
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
" named ", "uncolored", " parameter",
fgText=c("lightpink2","lightblue3"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
if (useColor && "NULL" %in% class(argTextA)) {
## For NULL we color using colNULL
deTextA <- paste0(
make_styles(text=c(name, "=", as.character(deTextA)),
style=c(col2, NA, colNULL),
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE"),
collapse="");
} else if (length(as.character(deTextA)) > 0 &&
nchar(as.character(deTextA)) > 0) {
if (useColor) {
deTextA <- paste0(
make_styles(text=c(name, "=", as.character(deTextA)),
style=c(col2, NA, col1),
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE"),
collapse="");
} else {
deTextA <- paste0(
c(name,
"=",
as.character(deTextA)),
collapse=" ");
}
} else {
## No parameter value, just the name
## as used for mandatory function arguments
if (useColor) {
deTextA <- paste0(
make_styles(text=c(name),
style=c(col2),
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE"),
collapse="");
} else {
deTextA <- paste0(c(name),
collapse=" ");
}
}
} else {
if (verbose) {
printDebug(indent, "", "handleArgsText(): ",
"unnamed ", "uncolored", " parameter",
fgText=c("lightpink2","lightblue3"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
if (useColor) {
if ("NULL" %in% class(argTextA)) {
deTextA <- paste0(
make_styles(text=as.character(deTextA),
style=colNULL,
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE"),
collapse="");
} else {
deTextA <- paste0(
make_styles(text=as.character(deTextA),
style=col1,
adjustRgb=adjustRgb,
Lrange=Lrange,
Crange=Crange,
setOptions="FALSE"),
collapse="");
}
} else {
deTextA <- paste0(as.character(deTextA),
collapse="");
}
}
}
if (debug > 0 && verbose) {
printDebug(indent, "", "handleArgsText(): ",
"deTextA (after make_styles):", deTextA,
fgText=c("lightskyblue3","lightseagreen"),
Crange=Crange,Lrange=Lrange,adjustRgb=adjustRgb);
}
}
return(deTextA);
}
#' Apply noise floor and ceiling to numeric vector
#'
#' Apply noise floor and ceiling to numeric vector
#'
#' A noise floor is useful when detected numeric values are sometimes below
#' a clear noise threshold, and where some downstream ratio may be calculated
#' using these values. Applying a noise floor ensures the ratios and not
#' artificially higher, especially in cases where the values involved are
#' least reliable. This procedure is expected to produce more conservative
#' and appropriate ratios in that scenario.
#'
#' A ceiling is similar, values above the ceiling are set to the ceiling,
#' which is practical when values above a certain threshold are conceptually
#' similar to those at the threshold. One clear example is plotting
#' `-log10(Pvalue)` when the range of P-values might approach 1e-1000.
#' In this case, setting a ceiling of 50 conceptually equates P-values
#' below 1e-50, while also restricting the axis range of a plot.
#'
#' The ability to set values at the floor to a different value, using
#' `newValue` different from `minimum`, is intended to allow separation
#' of numeric values from the floor for illustrative purposes.
#'
#' @return
#' A `numeric` vector or `matrix`, matching the input type `x` where numeric
#' values are fixed to the `minimum` and `ceiling` values as defined
#' by `newValue` and `newCeiling`, respectively.
#'
#' @family jam numeric functions
#'
#' @param x `numeric` vector or matrix
#' @param minimum `numeric` floor value
#' @param newValue `numeric`, by default the same as the floor value. Sometimes
#' it can be useful to define a different value, one example is to define
#' values as `NA`, or another distinct number away from the floor.
#' @param adjustNA `logical` whether to change `NA` values to the `newValue.`
#' @param ceiling `numeric` value, optionally a ceiling. If defined, then values
#' above the ceiling value are set to `newCeiling.`
#' @param newCeiling `numeric` value when ceiling is defined, values above the
#' ceiling are set to this `numeric` value.
#' @param ... additional parameters are ignored.
#'
#' @examples
#' # start with some random data
#' n <- 2000;
#' x1 <- rnorm(n);
#' y1 <- rnorm(n);
#'
#' # apply noise floor and ceiling
#' x2 <- noiseFloor(x1, minimum=-2, ceiling=2);
#' y2 <- noiseFloor(y1, minimum=-2, ceiling=2);
#'
#' # apply noise floor and ceiling with custom replacement values
#' xm <- cbind(x=x1, y=y1);
#' xm3 <- noiseFloor(xm,
#' minimum=-2, newValue=-3,
#' ceiling=2, newCeiling=3);
#'
#' parMfrow <- par("mfrow");
#' par("mfrow"=c(2,2));
#' plotSmoothScatter(x1, y1);
#' plotSmoothScatter(x2, y2);
#' plotSmoothScatter(xm3);
#' par("mfrow"=parMfrow);
#'
#' @export
noiseFloor <- function
(x,
minimum=0,
newValue=minimum,
adjustNA=FALSE,
ceiling=NULL,
newCeiling=ceiling,
...)
{
## Purpose is to apply a noise floor, that is, to set all values
## to be at least 'minimum' amount.
## This function performs no scaling or normalization.
if (length(x) == 0) {
return(x);
}
if (length(minimum) > 0) {
if (adjustNA) {
x[is.na(x) | (!is.na(x) & x < minimum)] <- newValue;
} else {
x[!is.na(x) & x < minimum] <- newValue;
}
}
if (length(ceiling) > 0) {
x[!is.na(x) & x > ceiling] <- newCeiling;
}
return(x);
}
#' Convert radians to degrees
#'
#' Convert radians to degrees
#'
#' This function simply converts radians which range from zero to pi*2,
#' into degrees which range from 0 to 360.
#'
#' @param x `numeric` vector, expected to be radian values between zero
#' and pi*2.
#' @param ... other parameters are ignored.
#'
#' @family jam numeric functions
#'
#' @examples
#' rad2deg(c(pi*2, pi/2))
#'
#' @export
rad2deg <- function
(x, ...)
{
## Purpose is to convert radians to degrees, using pi/2 as 90 degrees
x * (180 / pi);
}
#' Convert degrees to radians
#'
#' Convert degrees to radians
#'
#' This function simply converts degrees which range from 0 to 360,
#' into radians which range from zero to pi*2.
#'
#' @param x `numeric` vector, expected to be degree values between zero
#' and 360.
#' @param ... other parameters are ignored.
#'
#' @family jam numeric functions
#'
#' @examples
#' deg2rad(rad2deg(c(pi*2, pi/2)))/pi;
#'
#' @export
deg2rad <- function
(x, ...)
{
## Purpose is to convert degrees to radians, using pi/2 as 90 degrees
x * (pi / 180);
}
#' print dimensions of list object elements
#'
#' @description
#' `sdim()` prints the name and dimensions of `list` object elements,
#' such as a `list` of `data.frame`
#'
#' `ssdim()` prints the name and dimensions of nested elements of `list`
#' objects, for example a `list` of `list` objects that each contain
#' other objects.
#'
#' `sdima()` prints the name and dimensions of object `attributes(x)`.
#' It is useful for summarizing the `attributes()` of an object.
#'
#' `ssdima()` prints the name and dimensions of nested elements of `list`
#' object `attributes()`, for example a `list` of `list` objects that each
#' contain other objects. It is useful for comparing attributes across `list`
#' elements.
#'
#' This function prints the dimensions of a list of objects, usually a `list`
#' of `data.frame` objects, but extended to handle more complicated lists,
#' including even S4 object `slotNames()`.
#'
#' Over time, more object types will be made compatible with this function.
#' Currently, `igraph` objects will print the number of nodes and edges, but
#' requires the igraph package to be installed.
#'
#' @param x one of several recognized object classes:
#' * an S3 object inheriting from class `"list"`, including a nested list of
#' lists or simple list
#' * an S3 atomic object, which returns only the length
#' * a single multi-dimensional object such as `data.frame`, `matrix`,
#' `array`, `tibble`, or similar, which returns only its dimensions.
#' * an `S4` object in which case it used `slotNames(x)`
#' to traverse the object structure
#' * an `"environment"` object, in which case `ls(envir=x)` is
#' used to traverse the object structure.
#' * When the object is `S4` that inherits `"List"` from the
#' `S4Vectors` package, it will attempt to use the proper subset
#' functions from `S4Vectors` via `names(x)`, but that process only works
#' properly if the `S4Vectors` package is previously loaded,
#' otherwise it reverts to using `slotNames(x)`.
#' @param includeClass `logical` indicating whether to print the class of
#' each element in the input \code{x} object. Note that for S4 objects,
#' each element will be the object returned for each of \code{slotNames(x)}.
#' @param doFormat `logical` indicating whether to format the dimensions using
#' \code{format(...,big.mark=",")}, which is mainly useful for extremely
#' large dimensions. This parameter should probably become more broadly
#' useful and respectful for different locales.
#' @param big.mark `character` value used when `doFormat=TRUE`, used in the
#' call to `format(...,big.mark)`.
#' @param verbose `logical` whether to print verbose output
#' @param ... additional parameters are ignored.
#'
#' @return `data.frame` where each row indicates the dimensions of
#' each element in the input list. When `includeClass` is `TRUE` it
#' will include a column `class` which indicates the class of each
#' list element. When the input list contains arrays with more than
#' two dimensions, the first two dimensions are named `"rows"` and
#' `"columns"` with additional dimensions named `"dim3"` and so on.
#' Any list element with fewer than that many dimensions will only have
#' values populated to the relevant dimensions, for example a character
#' vector will only populate the length.
#'
#' @family jam practical functions
#' @family jam list functions
#'
#' @examples
#' L <- list(LETTERS=LETTERS,
#' letters=letters,
#' lettersDF=data.frame(LETTERS, letters));
#' sdim(L)
#'
#' LL <- list(L=L, A=list(1:10))
#' sdim(LL)
#' ssdim(LL)
#'
#' m <- matrix(1:9,
#' ncol=3,
#' dimnames=list(
#' Rows=letters[1:3],
#' Columns=LETTERS[1:3]));
#' sdima(m);
#' ssdima(m);
#'
#' @export
sdim <- function
(x,
includeClass=TRUE,
doFormat=FALSE,
big.mark=",",
verbose=FALSE,
...)
{
## Purpose is to take a list of data.frames and return the dim() of each.
##
## It does try to work with other object types, returning the length for
## most which are not data.frame, matrix, tibble.
##
## includeClass is logical value indicating whether to return the class
## of each object in its own column, which provides a useful summary
## of the list elements.
##
## If the object is not "list" class, and has slotNames(x), then
## they will be used instead
##
getDim <- function
(i,
doFormat=FALSE,
includeClass=FALSE,
big.mark=",",
...)
{
## Simply a wrapper function
iClass <- class(i);
## igraph objects return the number of vertices and edges
if (igrepHas("igraph", iClass)) {
if (!check_pkg_installed("igraph")) {
stop("The igraph package is required to describe igraph objects.");
}
iDim <- c(igraph::vcount(i),
igraph::ecount(i));
} else {
iDim <- tryCatch({
dim(i);
}, error=function(e){
length(i);
});
if (length(iDim) == 0) {
iDim <- length(i);
}
}
names(iDim) <- head(
c("rows",
"cols",
makeNames(renameOnes=TRUE, startN=3, suffix="",
tail(rep("dim", length.out=length(iDim)), -2))),
length(iDim));
if (doFormat) {
iDim <- format(iDim,
big.mark=big.mark,
trim=TRUE,
...);
}
iDim <- as.list(iDim);
if (includeClass) {
iClass <- nameVector(class(i),
"class",
renameFirst=FALSE,
startN=2);
iDim <- c(as.list(iClass),
iDim);
}
data.frame(iDim);
}
## Special case for S4 objects with only one slotName ".Data"
if (isS4(x)) {
sn1 <- slotNames(x);
xn1 <- names(x);
if (".Data" %in% sn1 && length(sn1) == 1) {
if (verbose) {
printDebug("sdim(): ",
"Coercing S4 class using ",
"list(x@.Data)");
}
xl <- slot(x, ".Data");
## If there are no names, and the names(x) are the
## proper length, assign names(x) to the list we create.
if (length(names(xl)) == 0 && length(xl) == length(xn1)) {
names(xl) <- xn1;
}
x <- xl;
rm(xl);
}
}
## Iterate each element and determine the dimensions
#if (!igrepHas("list|tbl|tibble|data.frame|matrix", class(x)) &&
# isS4(x)) {
x_is_table <- igrepHas("tbl|tibble|data.frame|matrix|data.table|dataframe",
class(x));
if (isS4(x) && !x_is_table) {
## Wrap in tryCatch() in case the supporting object package
## is not installed, otherwise just use slotNames(x).
is_List <- tryCatch({
inherits(x, "List") && length(x[[1]]) >= 0;
}, error=function(e){
FALSE;
});
if (is_List) {
sn1 <- nameVector(seq_along(x), names(x));
if (verbose) {
printDebug("sdim(): ",
"Coercing S4 List using ",
"names(x)");
}
} else {
sn1 <- nameVector(slotNames(x));
if (verbose) {
printDebug("sdim(): ",
"Coercing S4 class using ",
"slotNames(x)");
}
}
sdL <- lapply(sn1, function(sni){
if (is_List) {
i <- x[[sni]];
} else {
i <- slot(x, sni);
}
iDim <- getDim(i,
doFormat=doFormat,
includeClass=includeClass,
big.mark=big.mark,
...);
iDim;
});
} else if (is.environment(x)) {
## handle environment by using ls(x) which maintains order,
## instead of names(x) which is in random order
sdL <- lapply(nameVector(ls(x)), function(i){
iDim <- getDim(
get(i, envir=x),
doFormat=doFormat,
includeClass=includeClass,
big.mark=big.mark,
...);
iDim;
});
} else {
## If it is a vector and not an inherited form of list
if ((is.atomic(x) && !is.recursive(x)) ||
x_is_table) {
if (verbose) {
printDebug("sdim(): ",
"Coercing class(x) from ",
class(x),
" to list(x)");
printDebug("x_is_table:", x_is_table);
printDebug("is.vector(x):", is.vector(x));
printDebug("igrepHas('list', class(x)):",
igrepHas("list", class(x)));
printDebug("is.list(x):", is.list(x));
}
x <- list(x);
}
if (verbose) {
printDebug("sdim(): ",
"length(x):",
length(x));
}
sdL <- lapply(x, function(i){
iDim <- getDim(i,
doFormat=doFormat,
includeClass=includeClass,
big.mark=big.mark,
...);
iDim;
});
}
if (includeClass) {
sdLnames <- unique(unlist(lapply(sdL, names)));
sdLnamesC <- vigrep("^class", sdLnames);
sdLnamesD <- unvigrep("^class", sdLnames);
sdLnames <- c(sdLnamesD, sdLnamesC)
sdL <- lapply(sdL, function(i){
df2 <- data.frame(lapply(nameVector(sdLnames), function(i)NA));
df2[,colnames(i)] <- i;
df2;
});
}
sd1 <- rbindList(sdL);
sd1$rows <- rmNA(naValue="", sd1$rows);
sd1$cols <- rmNA(naValue="", sd1$cols);
return(sd1);
}
#' @rdname sdim
#'
#' @export
sdima <- function
(x,
includeClass=TRUE,
doFormat=FALSE,
big.mark=",",
verbose=FALSE,
...)
{
#
sdim(attributes(x),
includeClass=includeClass,
doFormat=doFormat,
big.mark=big.mark,
verbose=verbose,
...);
}
#' @rdname sdim
#'
#' @return `list` of `data.frame` each of which
#' describes the dimensions of the objects in
#' `attributes(x)`.
#'
#' @export
ssdima <- function
(x,
includeClass=TRUE,
doFormat=FALSE,
big.mark=",",
verbose=FALSE,
...)
{
#
ssdim(attributes(x),
includeClass=includeClass,
doFormat=doFormat,
big.mark=big.mark,
verbose=verbose,
...);
}
#' @rdname sdim
#'
#' @return `list` of `data.frame`, each row indicates the dimensions of
#' each element in the input list.
#' When `includeClass` is `TRUE` it
#' will include a column `class` which indicates the class of each
#' list element.
#' When the input `list` contains arrays with more than
#' two dimensions, the first two dimensions are named `"rows"` and
#' `"columns"` with additional dimensions named `"dim3"` and so on.
#' Any `list` element with fewer than that many dimensions will only have
#' values populated to the relevant dimensions, for example a character
#' vector will only populate the length.
#'
#' @export
ssdim <- function
(x,
includeClass=TRUE,
doFormat=FALSE,
big.mark=",",
verbose=FALSE,
...)
{
## Purpose is to run sdim() on a list of lists of data.frames
## Special case for S4 objects with only one slotName ".Data"
if (isS4(x)) {
sn1 <- slotNames(x);
xn1 <- names(x);
if (".Data" %in% sn1 && length(sn1) == 1) {
if (verbose) {
printDebug("ssdim(): ",
"Coercing S4 class using ",
"list(x@.Data)");
}
xl <- slot(x, ".Data");
## If there are no names, and the names(x) are the
## proper length, assign names(x) to the list we create.
if (length(names(xl)) == 0 && length(xl) == length(xn1)) {
names(xl) <- xn1;
}
x <- xl;
}
}
## S4 objects are handled as a list using slotNames(x)
if (isS4(x)) {
# For S4 objects we iterate slotNames(x)
if (verbose) {
printDebug("ssdim(): ",
"Handling S4 object type.");
}
## Wrap in tryCatch() in case the supporting object package
## is not installed, otherwise just use slotNames(x).
is_List <- tryCatch({
inherits(x, "List") && length(x[[1]]) >= 0;
}, error=function(e){
FALSE;
})
if (is_List) {
## List uses names(x)
lapply(nameVector(seq_along(x), names(x)), function(iName){
if (verbose) {
printDebug("ssdim(): ",
"S4 List item:",
iName,
", List name:",
names(x)[iName]);
}
sdim(x[[iName]],
includeClass=includeClass,
doFormat=doFormat,
big.mark=big.mark,
verbose=verbose);
});
} else {
lapply(nameVector(slotNames(x)), function(iName){
if (verbose) {
printDebug("ssdim(): ",
"slotName iName:",
iName);
}
sdim(slot(x, iName),
includeClass=includeClass,
doFormat=doFormat,
big.mark=big.mark,
verbose=verbose);
});
}
} else if (!any(c("list", "environment") %in% unlist(sclass(x)))) {
## No recognizable list structure
if (is.vector(x) ||
igrepHas("data.*frame|tibble|matrix|ranges$", class(x))) {
if (verbose) {
printDebug("ssdim(): ",
"Handling vector/data.frame/tibble/ranges.");
}
sdim(x);
} else if (length(names(x)) > 0) {
if (verbose) {
printDebug("ssdim(): ",
"Handling S3 object type using names(x).");
}
lapply(nameVector(names(x)), function(i){
sdim(x[[i]]);
});
} else if (length(x) > 1) {
if (verbose) {
printDebug("ssdim(): ",
"Handling S3 object type without names.");
}
lapply(seq_along(x), function(i){
sdim(x[[i]]);
});
} else {
if (verbose) {
printDebug("ssdim(): ",
"Falling back to sdim(x).");
}
sdim(x);
}
} else {
if (verbose) {
printDebug("ssdim(): ",
"Handling list object type.");
}
lapply(x, function(iDFL){
sdim(iDFL,
includeClass=includeClass,
doFormat=doFormat,
big.mark=big.mark,
verbose=verbose);
});
}
}
#' return the classes of a list of objects
#'
#' return the classes of a list of objects
#'
#' This function takes a `list` and returns the classes for each
#' object in the list. In the event an object class has multiple values,
#' the returned object is a list, otherwise is a vector.
#' If `x` is an S4 object, then `slotNames(x)` is used, and
#' the class is returned for each S4 slot.
#'
#' When `x` is a `data.frame`, `data.table`, `tibble`, or similar
#' `DataFrame` table-like object, the class of each column is returned.
#'
#' For the special case where `x` is an S4 object with one slotName
#' `".Data"`, the values in `x@.Data` are coerced to a `list`. One
#' example of this case is with `limma::MArrayLM-class`.
#'
#' When `x` is a matrix, the class of each column is returned for
#' consistency, even though the class of each column should be identical.
#'
#' For more more information about a list-like object, including
#' the lengths/dimensions of the elements, see `sdim()` or `ssdim()`.
#'
#' @return `character` vector with the class of each list element, or
#' column name, depending upon the input `class(x)`.
#'
#' @param x an S3 object inheriting from class `list`, or an S4 object.
#' @param ... additional parameters are ignored.
#'
#' @family jam practical functions
#' @family jam list functions
#'
#' @examples
#' sclass(list(LETTERS=LETTERS, letters=letters));
#'
#' sclass(data.frame(B=letters[1:10], C=2:11))
#'
#' @export
sclass <- function
(x,
...)
{
## Purpose is to take a list of objects and return the class() of each.
##
## If the object is not "list" class, and has slotNames(x), then
## they will be used instead
if (isS4(x)) {
sn1 <- slotNames(x);
xn1 <- names(x);
if (".Data" %in% sn1 && length(sn1) == 1) {
## Special case of x@.Data containing a list
xl <- slot(x, ".Data");
if (length(xl) == length(xn1)) {
names(xl) <- xn1;
}
x <- xl;
} else {
sd1 <- sapply(sn1, function(sni){
class(slot(x, sni));
});
}
}
if (igrepHas("matrix", class(x))) {
sd1 <- sapply(nameVector(colnames(x)), function(i){
class(x[,i])
});
} else if (!isS4(x)) {
sd1 <- sapply(x, function(i){
class(i);
});
}
sd1;
}
#' Scale a numeric vector from 0 to 1
#'
#' Scale a numeric vector from 0 to 1
#'
#' This function is intended as a quick way to scale numeric values
#' between 0 and 1, however other ranges can be defined as needed.
#'
#' NA values are ignored and will remain NA in the output. To handle
#' NA values, use the `rmNA()` function, which can optionally replace
#' NA with a fixed numeric value.
#'
#' The parameters `low` and `high` are used optionally to provide a
#' fixed range of values expected for `x`, which is useful for
#' consistent scaling of `x`. Specifically, if `x` may be a
#' vector of numeric values ranging from 0 and 100, you would
#' define `low=0` and `high=100` so that `x` will be consistently
#' scaled regardless what actual range is represented by `x`.
#'
#' Note that when `x` contains only one value, and `low` and `high`
#' are not defined, then `x` will be scaled based upon the
#' argument `singletMethod`. For example, if you provide `x=2`
#' and want to scale `x` values to between 0 and 10... `x` can
#' either be the `mean` value `5`; the `min`imum value `0`; or
#' the `max`imum value `10`.
#'
#' However, if `low` or `high` are defined, then x will be scaled
#' relative to that range.
#'
#' @param x `numeric` vector.
#' @param from the minimum `numeric` value to re-scale the input numeric vector.
#' @param to the maximum `numeric` value to re-scale the input numeric vector.
#' @param low `numeric` value defining the low end of the input numeric range,
#' intended when input values might not contain the entire numeric
#' range to be re-scaled.
#' @param high `numeric` value defining the high end of the input numeric range,
#' intended when input values might not contain the entire numeric
#' range to be re-scaled.
#' @param naValue optional `numeric` value used to replace `NA`, usually by
#' replacing `NA` with zero.
#' @param singletMethod `character` value describing how to handle singlet
#' input values, for example how to scale the number 5 by itself.
#' * "mean" then it uses the average of `from` and `to`,
#' * "min" uses the `from` value, and
#' * "max" uses the `to` value.
#' @param ... additional parameters are ignored.
#'
#' @family jam numeric functions
#'
#' @examples
#' # Notice the first value 1 is re-scaled to 0
#' normScale(1:11);
#'
#' # Scale values from 0 to 10
#' normScale(1:11, from=0, to=10);
#'
#' # Here the low value is defined as 0
#' normScale(1:10, low=0);
#'
#' normScale(c(10,20,40,30), from=50, to=65);
#'
#' @export
normScale <- function
(x,
from=0,
to=1,
low=min(x, na.rm=TRUE),
high=max(x, na.rm=TRUE),
naValue=NA,
singletMethod=c("mean","min","max"),
...)
{
## Purpose is to scale values between 0 and 1
## or optionally into a given range.
## naValue is used to change any NA values to this number.
##
## Note data can be scale to a custom range, using from and to.
##
## Note data can also be scaled to a range, using fixed reference points,
## using low and high. For example, you may want values at zero to stay zero,
## even if there are negative values.
##
## If only one value is supplied, then singletMethod is used:
## singletMethod="mean" the resulting value is the mean(c(from,to))
## singletMethod="max" the resulting value is the max(c(from,to))
## singletMethod="min" the resulting value is the min(c(from,to))
##
## You can therefore scale data using fixed points to a new fixed range,
## For example you may want a value of 100 to be scaled to 5, and values
## at zero to remain zero, with negative values also being scale accordingly.
## normScale(x, from=0, to=5, low=0, high=100);
##
#x <- ((x - min(x, na.rm=na.rm)) / diff(range(x, na.rm=na.rm)) * diff(c(from,to))) + from;
##
## If given low and high, and they are different (meaning there is
## more than one input value), then we honor that range. This way
## we can receive a single value, along with a defined low and high,
## and process it consistently
# process NA input
if (all(is.na(x))) {
return(NA);
}
if (length(low) > 0 && length(high) > 0 && low == high) {
singletMethod <- match.arg(singletMethod);
if (singletMethod %in% "mean") {
x[!is.na(x)] <- mean(c(from, to));
} else if (singletMethod %in% "max") {
x[!is.na(x)] <- max(c(from, to));
} else if (singletMethod %in% "max") {
x[!is.na(x)] <- min(c(from, to));
}
} else {
x <- ((x - low) / diff(range(c(low,high))) * diff(c(from,to))) + from;
if (!is.na(naValue) && any(is.na(x))) {
x[is.na(x)] <- naValue;
}
}
return(x);
}
#' Warp a vector of numeric values relative to zero
#'
#' Warp a vector of numeric values relative to zero
#'
#' This function warps numeric values using a log curve
#' transformation, such that values are either more compressed
#' near zero, or more compressed near the maximum values.
#' For example, a vector of integers from -10 to 10 would be warped
#' so the intervals near zero were smaller than 1, and intervals
#' farthest from zero are greater than 1.
#'
#' The main driver for this function was the desire to compress
#' divergent color scales used in heatmaps, in order to enhance
#' smaller magnitude numeric values. Existing color ramps map the
#' color gradient in a linear manner relative to the numeric range,
#' which can cause extreme values to dominate the color scale.
#' Further, a linear application of colors is not always appropriate.
#'
#' @param x `numeric` vector
#' @param lens `numeric` value which defines the lens factor,
#' where `lens > 0` will compress values near zero, and
#' `lens < 0` will expand values near zero and compress
#' values near the maximum value. If `lens == 0` the
#' numeric values are not changed.
#' @param baseline `numeric` value describing the baseline, for example
#' when the central value is non-zero. The baseline is subtracted
#' from `x`, the warp is applied, then the baseline is added to
#' the result.
#' @param xCeiling `numeric` maximum value used for the color warp range,
#' useful for consistency. When `xCeiling` is not supplied, the
#' maximum difference from `baseline` is used. When `xCeiling` is
#' defined, and `baseline` is non-zero, the effective value used
#' is `(xCeiling - baseline)`.
#'
#' @family jam numeric functions
#'
#' @examples
#' x <- c(-10:10);
#' xPlus10 <- warpAroundZero(x, lens=10);
#' xMinus10 <- warpAroundZero(x, lens=-10);
#'
#' plot(x=x, y=xPlus10, type="b", pch=20, col="dodgerblue",
#' main="Comparison of lens=+10 to lens=-10");
#' points(x=x, y=xMinus10, type="b", pch=18, col="orangered");
#' abline(h=0, v=0, col="grey", lty="dashed", a=0, b=1);
#' legend("topleft",
#' legend=c("lens=+10", "lens=-10"),
#' col=c("dodgerblue","orangered"),
#' pch=c(20,18),
#' lty="solid",
#' bg="white");
#'
#' # example showing the effect of a baseline=5
#' xPlus10b5 <- warpAroundZero(x, lens=10, baseline=5);
#' xMinus10b5 <- warpAroundZero(x, lens=-10, baseline=5);
#' plot(x=x, y=xPlus10b5, type="b", pch=20, col="dodgerblue",
#' main="Comparison of lens=+10 to lens=-10",
#' ylim=c(-10,15),
#' sub="baseline=+5");
#' points(x=x, y=xMinus10b5, type="b", pch=18, col="orangered");
#' abline(h=5, v=5, col="grey", lty="dashed", a=0, b=1);
#' legend("topleft",
#' legend=c("lens=+10", "lens=-10"),
#' col=c("dodgerblue","orangered"),
#' pch=c(20,18),
#' lty="solid",
#' bg="white");
#'
#' @export
warpAroundZero <- function
(x,
lens=5,
baseline=0,
xCeiling=NULL,
...)
{
## Purpose is to take a vector representing points on
## a line with slope=0, and warp the line using log2
## transformation, symmetric around zero.
if (lens == 0) {
return(x);
}
if (length(baseline) == 1 && is.numeric(baseline)) {
x <- x - baseline;
} else {
baseline <- 0;
}
nColors <- 50;
if (length(xCeiling) == 0) {
xCeiling <- max(abs(x), na.rm=TRUE);
} else {
xCeiling <- max(abs(xCeiling) - baseline);
}
if (any(abs(x) > abs(xCeiling))) {
xOob <- (abs(x) > abs(xCeiling));
x[xOob] <- abs(x[xOob]) * sign(x[xOob]);
}
x1 <- seq(from=-1, to=1, length.out=nColors);
y1 <- normScale(
log2(1+abs(x1)*abs(lens))*sign(x1),
from=-1,
to=1);
if (lens > 0) {
y1 <- approx(x=y1, y=x1, xout=x1)$y;
}
approx(x=x1 * (xCeiling),
y=y1 * (xCeiling),
xout=x)$y + baseline;
}
#' lengths for recursive lists
#'
#' lengths for recursive lists
#'
#' This function takes a list as input, and returns the length
#' of each list element after running `base::unlist()`.
#'
#' @return
#' * When `doSum is NULL` (default) it returns an `integer` vector
#' with length `length(x)` and names `names(x)`,
#' whose values are the total number of elements in each item in
#' `x` after running `base::unlist()`.
#' * When `doSum=="TRUE"`, it returns the single `integer` length of
#' all elements in `x`.
#' * When `doSum=="FALSE"`, it returns the full structure of `x` with the
#' `integer` length of each element.
#'
#' The parameter `doSum` is intended for internal use, during
#' recursive calls of `rlengths()` to itself. When `doSum is NULL` or
#' `TRUE`, recursive calls to `rlengths()` set `doSum=TRUE`.
#'
#' @family jam list functions
#'
#' @param x `list` or vector
#' @param doSum `logical` indicating whether to return the overall sum
#' of lengths. When `NULL` it will return the aggregate length of
#' each list element in `x`. When `FALSE` it will return the same
#' list structure of x, with the length of each. When `TRUE` it will
#' return the total length of all elements in `x` as one value.
#' @param ... additional parameters are ignored
#'
#' @examples
#' x <- list(
#' A=list(
#' A1=nameVector(1:3, letters[1:3]),
#' A2=list(
#' A1a=nameVector(4:7, letters[4:7]),
#' A1b=nameVector(11:14, letters[11:14]))),
#' B=list(B1=nameVector(1:9, letters[1:9]),
#' B2=nameVector(20:25, letters[20:25])));
#' # default lengths(x) shows length=2 for A and B
#' lengths(x)
#' # rlengths(x) shows the total length of A and B
#' rlengths(x)
#'
#' @export
rlengths <- function
(x,
doSum=NULL,
...)
{
## Purpose is to provide recursive lengths() for nested lists
##
# x <- list(A=list(A1=nameVector(1:3, letters[1:3]), A2=nameVector(4:7, letters[4:7])),
# B=list(B1=nameVector(1:9, letters[1:9]), B2=nameVector(20:25, letters[20:25])))
rl <- lapply(x, function(i){
if (length(doSum) == 0) {
doSum <- TRUE;
}
## Note: the algorithm behaves unexpectedly with different exotic
## classes embedded inside a list
if (!igrepHas("function", class(i)) &&
(igrepHas("list", class(i)) ||
any(lengths(i) > 1))) {
rlengths(i, doSum=doSum, ...);
} else {
length(i)
}
});
if (length(doSum) > 0) {
if (doSum) {
rl <- do.call(sum, rl);
}
} else {
rl <- unlist(rl);
}
return(rl);
}
#' Search for objects in the environment
#'
#' Search for objects in the environment
#'
#' This function searches the active R environment for an object name
#' using `vigrep()` (value, case-insensitive grep).
#' It is helpful when trying to find an object using a
#' substring, for example `grepls("statshits")`.
#'
#' @param x `character` string used as a grep pattern
#' @param where `character` string compatible with `base::ls()` or if
#' installed, `AnnotationDbi::ls()`. A special value `"all"` will
#' search all environments on the search path `base::search()`
#' in order.
#' @param ignore.case `logical` indicating whether the pattern match
#' is case-insensitive.
#' @param searchNames `logical` indicating whether names should also
#' be searched, which is only relevant for `AnnDb` objects,
#' for example `org.Mm.egSYMBOL2EG` from the `org.Mm.eg.db`
#' Bioconductor package.
#' @param verbose `logical` indicating whether to print verbose output.
#' @param ... additional parameters are ignored.
#'
#' @return
#' `vector` of matching object names, or if `where="all"` a named list,
#' whose names indicate the search environment name, and whose
#' entries are matching object names within each environment.
#'
#' @family jam grep functions
#'
#' @examples
#' # Find all objects named "grep", which should find
#' # base grep() and jamba::vigrep() among other results.
#' grepls("grep");
#'
#' # Find objects in the local environment
#' allStatsHits <- c(1:12);
#' someStatsHits <- c(1:3);
#' grepls("statshits");
#' # shortcut way to search only the .GlobalEnv, the active local environment
#' grepls("statshits", 1);
#'
#' # return objects with "raw" in the name
#' grepls("raw");
#'
#' # Require "Raw" to be case-sensitive
#' grepls("Raw", ignore.case=FALSE)
#'
#' @export
grepls <- function
(x,
where="all",
ignore.case=TRUE,
searchNames=TRUE,
verbose=FALSE,
...)
{
## Purpose is to search for an object name in a variety of places
##
## where can be a package, in format "package:jamba"
## where can be "all" which uses everything in the search path `search()`
##
## where can be an AnnDb environment, for example
## grepls("Actb", org.Mm.egSYMBOL2EG)
##
## searchNames=TRUE only affects AnnDB objects, which are converted
## to list first, then searched by value and by name.
## grepls("Actb", org.Mm.egSYMBOL2EG, searchNames=FALSE)
##
if (igrepHas("character", class(where)) &&
"all" %in% where) {
if (verbose) {
printDebug("grepls(): ",
"Searching ",
'"all"');
}
searchL <- lapply(nameVector(search()), function(i){
ls(i);
});
# 0.0.93.900: improve grep() on list with regular expressions
searchLuse <- searchL[sapply(searchL, function(i){
igrepHas(x, i)
})]
# searchLuse <- searchL[igrep(x,
# searchL,
# ignore.case=ignore.case)];
lapply(searchLuse, function(i){
vigrep(x,
i,
ignore.case=ignore.case);
});
} else {
if (verbose) {
printDebug("grepls(): ",
"class(where):",
class(where));
}
if (searchNames && igrepHas("anndb", class(where))) {
## AnnDb objects are converted to lists, which lets us
## search the values and names together
if (verbose) {
printDebug("grepls(): ",
"Converted AnnDb to list.")
}
lsVal <- as.list(where);
} else {
lsVal <- ls(where);
#vigrep(x, lsVal);
}
c(lsVal[igrep(x,
lsVal,
ignore.case=ignore.case)],
lsVal[igrep(x,
names(lsVal),
ignore.case=ignore.case)]);
}
}
#' Return the newest file from a vector of files
#'
#' Return the newest file from a vector of files
#'
#' This function returns the newest file, defined by the most
#' recently modified time obtained from `base::file.info()`.
#'
#' @param x `character` vector of files, specifying file path where
#' required.
#' @param timecol `character` value from the output of `base::file.info()`
#' indicating the time column used to order files. By default `"mtime"`
#' refers to the time the file was last modified.
#' @param n `integer` number of files to return, in order of the most
#' recent to the least recent. By default `n=1` returns only the one
#' newest file.
#' @param ... additional parameters are ignored.
#'
#' @family jam practical functions
#'
#' @examples
#' newestFile(list.files());
#'
#' @return
#' Character vector `length=1` of the most recently modified file
#' from the input vector `x`. Note that any files not found are removed,
#' using `base::file.exists()`.
#'
#' @export
newestFile <- function
(x,
timecol="mtime",
n=1,
...)
{
## This function takes a vector of one or more files and returns
## the one file most recently modified.
##
## Files not found are removed from the input.
x <- x[file.exists(x)];
if (length(x) <= 1) {
return(x);
}
iFO <- file.info(x);
iFOorder <- rev(order(iFO[[timecol]]));
return(head(x[iFOorder], n));
}
#' Vectorized isFALSE
#'
#' Vectorized isFALSE
#'
#' This function applies three criteria to an input vector, to
#' determine if each entry in the vector is FALSE:
#'
#' 1. It must be class `logical`.
#' 2. It must not be `NA`.
#' 3. It must evaluate as `FALSE`.
#'
#' @param x vector
#' @param ... additional arguments are ignored
#'
#' @family jam practical functions
#'
#' @export
isFALSEV <- function
(x,
...)
{
## Purpose is to supply vectorized version of base::isFALSE()
(is.logical(x) & !is.na(x) & !x)
}
#' Vectorized isTRUE
#'
#' Vectorized isTRUE
#'
#' This function applies three criteria to an input vector, to
#' determine if each entry in the vector is TRUE:
#'
#' 1. It must be class `logical`.
#' 2. It must not be `NA`.
#' 3. It must evaluate as `TRUE`.
#'
#' @param x vector
#' @param ... additional arguments are ignored
#'
#' @family jam practical functions
#'
#' @export
isTRUEV <- function
(x,
...)
{
## Purpose is to supply vectorized version of base::isFALSE()
(is.logical(x) & !is.na(x) & x)
}
#' Paste data.frame rows into an ordered factor
#'
#' Paste data.frame rows into an ordered factor
#'
#' This function is an extension to `jamba::pasteByRow()` which
#' pastes rows from a `data.frame` into a character vector. This
#' function defines factor levels by running `jamba::mixedSortDF(unique(x))`
#' and calling `jamba::pasteByRow()` on the result. Therefore the
#' original order of the input `x` is maintained while the factor
#' levels are based upon the appropriate column-based sort.
#'
#' Note that the `...` additional arguments are
#' passed to `jamba::mixedSortDF()` to customize the column-based
#' sort order, used to define factor levels. A good way to test the
#' order of factors is to run `jamba::mixedSortDF(unique(x))` with
#' appropriate arguments, and confirm the rows are ordered as expected.
#'
#' Note also that `jamba::mixedSortDF()` uses `jamba::mixedSort()`
#' which itself performs alphanumeric sort in order to keep
#' values in proper numeric order where possible.
#'
#' @param x `data.frame`
#' @param sep `character` separator to use between columns
#' @param na.rm `logical` whether to remove NA values, or include them as "NA"
#' @param condenseBlanks `logical` whether to condense blank or empty values
#' without including an extra delimiter between columns.
#' @param includeNames `logical` whether to include the colname delimited
#' prior to the value, using sepName as the delimiter.
#' @param keepOrder `logical` indicating whether non-factor columns
#' should order factor levels based upon the existing order of
#' unique items. This option is intended for `data.frame` whose
#' columns are already sorted in proper order, but where columns
#' are not `factor` with appropriate factor levels. Note that
#' even when `keepOrder=TRUE` all existing `factor` columns will
#' honor the order of factor levels already present in those
#' columns.
#' @param byCols `integer` or `character` passed to `mixedSortDF()`.
#' This argument defines the order of columns sorted by `mixedSortDF()`,
#' and does not affect the order of columns pasted. Columns are
#' always pasted in the same order they appear in `x`. This argument
#' `byCols` was previously passed via `...` but is added here
#' to make this connection more direct.
#' @param na.last `logical` passed to `base::factor()` to determine whether
#' `NA` values are first or last in factor level order.
#' @param ... additional arguments are passed to `jamba::pasteByRow()`,
#' and to `jamba::mixedSortDF()`.
#'
#' @family jam string functions
#'
#' @examples
#' f <- LETTERS;
#' df <- data.frame(A=f[rep(1:3, each=2)],
#' B=c(NA, f[3]),
#' C=c(NA, NA, f[2]))
#' df
#'
#' # note that output is consistent with mixedSortDF()
#' jamba::mixedSortDF(df)
#' jamba::pasteByRowOrdered(df)
#'
#' jamba::mixedSortDF(df, na.last=FALSE)
#' jamba::pasteByRowOrdered(df, na.last=FALSE)
#'
#' jamba::mixedSortDF(df, byCols=c(3, 2, 1))
#' jamba::pasteByRowOrdered(df, byCols=c(3, 2, 1))
#'
#' df1 <- data.frame(group=rep(c("Control", "ABC1"), each=6),
#' time=rep(c("Hour2", "Hour10"), each=3),
#' rep=paste0("Rep", 1:3))
#' # default will sort each column alphanumerically
#' pasteByRowOrdered(df1)
#'
#' # keepOrder=TRUE will honor existing order of character columns
#' pasteByRowOrdered(df1, keepOrder=TRUE)
#'
#' @export
pasteByRowOrdered <- function
(x,
sep="_",
na.rm=TRUE,
condenseBlanks=TRUE,
includeNames=FALSE,
keepOrder=FALSE,
byCols=seq_len(ncol(x)),
na.last=TRUE,
...)
{
## Purpose is to enhance pasteByRow() except maintain ordering of factors
## where applicable, or define the output as a factor with ordered levels,
## using mixedSortDF() which does alphanumeric ordering while maintaining
## pre-existing factor ordering.
xstr <- pasteByRow(x,
sep=sep,
na.rm=na.rm,
condenseBlanks=condenseBlanks,
includeNames=includeNames,
...);
# optionally convert non-factor columns to factor with levels
# based upon the order of existing unique values
if (length(keepOrder) > 0 && any(keepOrder)) {
reorder_cols <- grepl("character", ignore.case=TRUE, cPaste(sclass(x)));
if (any(reorder_cols)) {
for (i in which(reorder_cols)) {
x[[i]] <- factor(x[[i]],
levels=unique(x[[i]]));
}
}
}
# calculate level order by the same logic,
# except sort columns with mixedSortDF()
xlevels <- pasteByRow(
mixedSortDF(unique(x),
na.last=na.last,
byCols=byCols,
...),
sep=sep,
na.rm=na.rm,
condenseBlanks=condenseBlanks,
includeNames=includeNames,
...);
factor(xstr,
levels=xlevels);
}
#' Merge list of data.frames retaining all rows
#'
#' Merge list of data.frames retaining all rows
#'
#' This function is a wrapper around `base::merge.data.frame()`
#' except that it allows more than two data.frame objects,
#' and applies default arguments `all.x=TRUE` and `all.y=TRUE`
#' for each merge operation to ensure that all rows are kept.
#'
#' @family jam practical functions
#' @family jam list functions
#'
#' @return `data.frame` after iterative calls to `base::merge.data.frame()`.
#'
#' @param ... arguments are handled as described:
#' * named arguments are passed through to `base::merge.data.frame()`,
#' with the exception of `all.x` and `all.y` which are both defined
#' `all.x=TRUE` and `all.y=TRUE`.
#' and all other arguments are assumed
#' to be `data.frame` or equivalent, and are merged in order they
#' appear as arguments. The order of these `data.frame` objects
#' should not affect the output content, but will affect the row
#' and column order of the resulting `data.frame`.
#'
#' @examples
#' df1 <- data.frame(City=c("New York", "Los Angeles", "San Francisco"),
#' State=c("New York", "California", "California"))
#' df2 <- data.frame(Team=c("Yankees", "Mets", "Giants", "Dodgers"),
#' City=c("New York", "New York", "San Francisco", "Los Angeles"))
#' df3 <- data.frame(State=c("New York", "California"),
#' `State Population`=c(39.24e9, 8.468e9),
#' check.names=FALSE)
#' mergeAllXY(df1, df3, df2)
#'
#' df4 <- data.frame(check.names=FALSE,
#' CellLine=rep(c("ul3", "dH1A", "dH1B"), each=2),
#' Treatment=c("Vehicle", "Dex"))
#' df4$CellLine <- factor(df4$CellLine,
#' levels=c("ul3", "dH1A", "dH1B"))
#' df4$Treatment <- factor(df4$Treatment,
#' levels=c("Vehicle", "Dex"))
#' df5 <- data.frame(
#' Treatment=rep(c("Vehicle", "Dex"), each=3),
#' Time=c("0h", "12h", "24h"))
#' df6 <- data.frame(check.names=FALSE,
#' CellLine=c("ul3", "dH1A", "dH1B"),
#' Type=c("Control", "KO", "KO"))
#' mergeAllXY(df4, df5, df6)
#'
#' # note the factor order is maintained
#' mergeAllXY(df4, df5, df6)$CellLine
#' mergeAllXY(df4, df5)$Treatment
#'
#' # merge "all" can append rows to a data.frame
#' df4b <- data.frame(check.names=FALSE,
#' CellLine=rep("dH1C", 2),
#' Treatment=c("Vehicle", "Dex"))
#' mergeAllXY(df4, df4b)
#'
#' # factor order is maintained, new levels are appended
#' mergeAllXY(df4, df4b)$CellLine
#'
#' # merge proceeds except shows missing data
#' mergeAllXY(df4, df4b, df5, df6)
#'
#' # note that appending rows is tricky, the following is incorrect
#' df6b <- data.frame(check.names=FALSE,
#' CellLine="dH1C",
#' Type="KO")
#' mergeAllXY(df4, df4b, df5, df6, df6b)
#'
#' # but it can be resolved by merging df6 and df6b
#' mergeAllXY(df4, df4b, df5, mergeAllXY(df6, df6b))
#'
#' # it may be easier to recognize by sorting with mixedSortDF()
#' mixedSortDF(honorFactor=TRUE,
#' mergeAllXY(df4, df4b, df5, mergeAllXY(df6, df6b)))
#'
#' # again, factor order is maintained
#' mergeAllXY(df4, df4b, df5, sort=FALSE, mergeAllXY(df6, df6b))$CellLine
#'
#' # the result can be sorted properly
#' mixedSortDF(honorFactor=TRUE,
#' mergeAllXY(df4, df4b, df5, mergeAllXY(df6, df6b)))
#'
#' @export
mergeAllXY <- function
(...)
{
## Purpose is a simple wrapper to merge(..., all.x=TRUE, all.y=TRUE);
##
## But detect whether 'x' and 'y' are defined, and if not, then define them
inList <- list(...);
## name inList for any un-named entries
if (is.null(names(inList))) {
names(inList) <- makeNames(rep("x", length(inList)));
} else {
names(inList) <- makeNames(names(inList));
}
## Filter out parameters for the merge.data.frame() function
mergeArgs <- unvigrep("^(x|y|[.]{3})$",
names(formals(base::merge.data.frame)));
inListArgs <- inList[names(inList) %in% mergeArgs];
inList <- inList[!names(inList) %in% mergeArgs];
## Now un-nest the list of entries remaining, so they're all non-list entities
inList <- unnestList(inList);
## Filter for only data.frame or matrix like objects
inListClass <- sapply(inList, function(i){
class(i);
});
inList <- inList[igrep("data.*frame|matrix", inListClass)];
inListClass <- sapply(inList, function(i){
class(i);
});
## Convert anything not a data.frame into such an object (ha!)
if (any(!inListClass %in% c("data.frame"))) {
whichNonDF <- which(!inListClass %in% c("data.frame"));
inList[whichNonDF] <- lapply(inList[whichNonDF], function(i){
newDF <- as.data.frame(i);
rownames(newDF) <- rownames(i);
colnames(newDF) <- colnames(i);
newDF;
})
}
## Accept list of data.frames and run iterative merge on them
## Usually seen as one '...' element which is a list of data.frames
if (length(inList) == 1) {
return(inList[[1]]);
} else if (length(inList) == 200) {
## If two list elements, we just call merge() once using x and y like normal
x <- inList[[1]];
y <- inList[[2]];
if (length(inListArgs) > 0) {
x <- do.call(merge,
c(alist(x=x,
y=y,
all.x=TRUE,
all.y=TRUE),
inListArgs));
} else {
x <- merge(x,
y,
all.x=TRUE,
all.y=TRUE);
}
return(x);
} else if (length(inList) >= 2) {
## Run iterative merge() methods
x <- inList[[1]];
for (i in 2:length(inList)) {
y <- inList[[i]];
## If we have merge.data.frame() arguments to pass,
## use do.call() so we can separately pass those function arguments
if (length(inListArgs) > 0) {
x <- do.call(merge,
c(alist(x=x, y=y, all.x=TRUE, all.y=TRUE), inListArgs));
} else {
x <- merge(x, y, all.x=TRUE, all.y=TRUE);
}
}
return(x);
} else {
stop("Could not match input to expected types, e.g. list of data.frames or matrices.");
}
}
#' Un-nest a nested list into a simple list
#'
#' Un-nest a nested list into a simple list
#'
#' This function inspects a list, and unlists each entry
#' resulting in a simple list of non-list entries as a result.
#' Sometimes when concatenating lists together, one list gets
#' added as a list-of-lists. This function resolves that problem
#' by providing one flat list.
#'
#' @return `list` that has been flattened so that it contains
#' no `list` elements. Note that it may contain some list-like
#' objects such as `data.frame`, defined by `stopClasses`.
#'
#' @family jam list functions
#'
#' @param x `list` potentially containing nested lists.
#' @param addNames `logical` indicating whether to add names to
#' the list elements when names are not already present. When
#' `addNames=TRUE` and no names are present `unnamedBase` is
#' used to define names.
#' @param unnamedBase `character` value used as a base for naming any
#' un-named lists, using the format `makeNamesFunc(rep(unnamedBase, n))`.
#' @param sep `character` delimiter used between nested list names.
#' @param makeNamesFunc `function` that takes a character vector and returns
#' non-duplicated character vector of equal length. By default it
#' uses `jamba::makeNames()`.
#' @param stopClasses `vector` of classes that should not be un-nested,
#' useful in case some classes inherit list properties.
#' @param extraStopClasses `vector` of additional values for `stopClasses`,
#' created mostly to show that `options("jam.stopClasses")` can be
#' used to define `stopClasses`, for example when this function
#' is called but where arguments cannot be conveniently passed
#' through the calling function.
#' @param ... additional arguments are ignored.
#'
#' @examples
#' L <- list(A=letters[1:10],
#' B=list(C=LETTERS[3:9], D=letters[4:11]),
#' E=list(F=list(G=LETTERS[3:9], D=letters[4:11])));
#' L;
#'
#' # inspect the data using str()
#' str(L);
#'
#' unnestList(L);
#'
#' # optionally change the delimiter
#' unnestList(L, sep="|");
#'
#' # example with nested lists of data.frame objects
#' df1 <- data.frame(a=1:2, b=letters[3:4]);
#' DFL <- list(A=df1,
#' B=list(C=df1, D=df1),
#' E=list(F=list(G=df1, D=df1)));
#' str(DFL);
#' unnestList(DFL);
#' str(unnestList(DFL));
#'
#' # packageVersion() returns class "package_version"
#' # where is.list(packageVersion("base")) is TRUE,
#' # but it cannot ever be subsetted as a list with x[[1]],
#' # and thus it breaks this function
#' identical(is.list(packageVersion("base")), is.list(packageVersion("base"))[[1]])
#' unnestList(lapply(nameVector(c("base","graphics")), packageVersion))
#'
#' @export
unnestList <- function
(x,
addNames=FALSE,
unnamedBase="x",
parentName=NULL,
sep=".",
makeNamesFunc=makeNames,
stopClasses=c("dendrogram",
"data.frame",
"matrix",
"package_version",
"tbl",
"data.table"),
extraStopClasses=getOption("jam.stopClasses"),
...)
{
## Purpose is to take a list of lists, and un-nest them
## into a list with depth=1, but all the non-list elements
## contained within it
newList <- list();
stopClasses <- unique(c(stopClasses, extraStopClasses));
if (any(class(x) %in% stopClasses)) {
return(x);
}
## Create default names if they don't exist already
if (addNames) {
x_names <- names(x);
if (length(x_names) == 0) {
names(x) <- rep(unnamedBase, length(x));
}
emptyNamesX <- which(names(x) %in% c("", NA));
if (any(emptyNamesX)) {
names(x)[emptyNamesX] <- rep(unnamedBase, length(emptyNamesX));
}
}
## add prefix if it were specified
if (length(parentName) > 0) {
names(x) <- paste(parentName, names(x), sep=sep);
}
## Make sure names are unique
if (length(names(x)) > 0) {
names(x) <- makeNamesFunc(names(x), ...);
}
## Iterate each list until we hit a non-list entry
if (inherits(x, "list") || is.list(x)) {
jvals <- seq_along(x);
for (j in jvals) {
i <- x[[j]];
## If we reached a list, and if the class isn't something
## we want to allow (e.g. dendrogram) then unnest one layer deeper
##
## Note the use of tryCatch() which returns the original
## object upon error, which should catch infinite recursion.
if (inherits(i, "list") || is.list(i) && !any(class(i) %in% stopClasses)) {
if (1 == 2) {
i <- unnestList(x=i,
addNames=addNames,
unnamedBase=unnamedBase,
stopClasses=stopClasses,
parentName=j,
sep=sep,
makeNamesFunc=makeNamesFunc,
...);
} else {
i <- tryCatch({
unnestList(x=i,
addNames=addNames,
unnamedBase=unnamedBase,
stopClasses=stopClasses,
parentName=names(x[j]),
sep=sep,
makeNamesFunc=makeNamesFunc,
...);
}, error=function(e){
structure("error", class="try-error", condition=e);
});
if ("try-error" %in% class(i)) {
return(x);
i <- list(x[[j]]);
}
}
} else {
i <- list(i);
if (length(names(x[j])) > 0 && !names(x[j]) %in% c("", NA)) {
names(i) <- names(x[j]);
}
}
newList <- c(newList, i);
};
} else {
newList <- x;
}
newList;
}
#' log2 transformation with directionality
#'
#' log2 transformation with directionality
#'
#' This function applies a log2 transformation but maintains
#' the sign of the input data, allowing for log2 transformation
#' of negative values.
#'
#' The method applies an offset to the absolute value `abs(x)`,
#' in order to handle values between zero and 1, then applies
#' log2 transformation, then multiplies by the original sign
#' from `sign(x)`.
#'
#' The argument `offset` is used to adjust values, for example
#' `offset=1` will apply log2 transformation `log2(1 + x)`,
#' except using the absolute value of `x`. This method allows
#' for positive and negative input data to contain values
#' between 0 and 1, and between -1 and 0.
#'
#' This function could be described as applying
#' a log2 transformation of the "magnitude" of values in `x`,
#' while maintaining the positive or negative directionality.
#'
#' If any `abs(x)` are less than `offset` this function will
#' raise an error.
#'
#' @return numeric vector of log-transformed magnitudes.
#'
#' @param x `numeric` vector
#' @param offset `numeric` value added to the absolute values
#' of `x` prior to applying the log transformation.
#' @param base `numeric` value indicating the logarithmic base,
#' by default `2` in order to apply `base::log2()`.
#' @param ... additional arguments are ignored.
#'
#' @family jam practical functions
#'
#' @examples
#' x <- c(-100:100)/10;
#' log2signed(x);
#' plot(x=x, y=log2signed(x), xlab="x", ylab="log2signed(x)")
#'
#' @export
log2signed <- function
(x,
offset=1,
base=2,
...)
{
## Purpose is to transform numeric data using log2 transformation
## but where negative values are kept negative by log2-transforming
## the absolute value, then multiplying by the original sign.
if (length(x) == 0) {
return(x);
}
if (offset < 1 && any(abs(x) < 1)) {
stop(
paste0(
"Values in abs(x) less than offset ",
offset,
" cannot be transformed without losing direction.")
);
}
## Determine the sign(x)
x_sign <- sign(x);
## For now, do not convert sign 0 to sign 1.
#x_sign <- ifelse(x_sign == 0, 1, x_sign);
if (length(base) == 0 || all(unique(base) == 2)) {
return(log2(abs(x) + offset) * x_sign);
}
# Note: the conversion to different log base
#log2(abs(x) + offset) * x_sign / log2(base);
log(abs(x) + offset, base=base) * x_sign;
}
#' exponentiate log2 values with directionality
#'
#' exponentiate log2 values with directionality
#'
#' This function is the reciprocal to `log2signed()`.
#'
#' It #' exponentiates the absolute values of `x`,
#' then subtracts the `offset`, then multiplies results
#' by the `sign(x)`.
#'
#' The `offset` is typically used to maintain
#' directionality of values during log transformation by
#' requiring all absolute values to be `1` or larger, thus
#' by default `offset=1`.
#'
#' @return numeric vector of exponentiated values.
#'
#' @param x `numeric` vector
#' @param numeric `offset`, subtracted from exponentiated values
#' prior to multiplying by the `sign(x)`.
#' @param base `numeric` value indicating the logarithmic base used.
#' For example `base=2` indicates values were transformed using
#' `log2()`.
#' @param ... additional arguments are ignored.
#'
#' @family jam practical functions
#'
#' @examples
#' x <- c(-100:100)/10;
#' z <- log2signed(x);
#' #plot(x=x, y=z, xlab="x", ylab="log2signed(x)")
#' plot(x=x, y=exp2signed(z), xlab="x", ylab="exp2signed(log2signed(x))")
#' plot(x=z, y=exp2signed(z), xlab="log2signed(x)", ylab="exp2signed(log2signed(x))")
#'
#' @export
exp2signed <- function
(x,
offset=1,
base=2,
...)
{
## Purpose is to apply the appropriate reciprocal to log2signed()
## Determine the sign(x)
x_sign <- sign(x);
## Exponentiate
if (length(base) == 0 || all(unique(base) == 2)) {
(2^abs(x) - offset) * x_sign;
} else {
# Note: the equivalent of the reciprocal of converting log base
# (2^(abs(x) * log2(base)) - offset) * x_sign;
# or in terms of e:
# (exp(abs(x) + log(base)) - offset) * x_sign;
(base^abs(x) - offset) * x_sign;
}
}
#' Apply head() across each element in a list of vectors
#'
#' Apply head() across each element in a list of vectors
#'
#' Note that this function currently only operates on a list
#' of vectors. This function is notably faster than
#' `lapply(x, head, n)` because it operates on the entire
#' vector in one step.
#'
#' Also the input `n` can be a vector so that each element in
#' the list has a specific number of items returned.
#'
#' @return `list` with at most `n` elements per vector.
#'
#' @family jam practical functions
#' @family jam list functions
#'
#' @param x `list` of atomic vectors, assumed to be the same
#' atomic type.
#' @param n `integer` maximum number of items to include from
#' each element in the list `x`. When `n` contains multiple
#' values, they are recycled to `length(x)` and applied to each
#' list element in order.
#' @param ... additional arguments are passed to `utils::head()`.
#'
#' @examples
#' l <- list(a=1:10, b=2:5, c=NULL, d=1:100);
#' heads(l, 1);
#'
#' heads(l, 2);
#'
#' heads(l, n=c(2, 1, 3, 5))
#'
#' @export
heads <- function
(x,
n=6,
...)
{
if (!is.list(x)) {
stop("Input must be a list.");
}
if (length(x) == 0) {
return(x)
}
if (length(x) == 1) {
x[[1]] <- head(x[[1]], n=head(n, 1), ..,);
return(x);
}
if (!is.atomic(x[[1]]) || !is.atomic(x[[(length(x))]])) {
stop("Input must be a list of atomic vectors.");
}
if (length(names(x)) == 0) {
xnames <- seq_along(x)
} else {
xnames <- factor(names(x), levels=names(x));
}
xidx <- rep(xnames, lengths(x));
xlen <- unlist(unname(lapply(split(xidx, xidx), seq_along)));
# Optionally expand n to be applied to each list element in order
if (length(n) > 1 && length(unique(n)) > 1) {
n <- rep(n, length.out=length(x))
n <- rep(n, lengths(x));
}
xkeep <- (xlen <= n);
xfull <- unlist(unname(x));
xnew <- split(xfull[xkeep], xidx[xkeep]);
if (length(names(x)) == 0) {
names(xnew) <- NULL;
}
xnew
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.