Nothing
#
# colourtables.R
#
# support for colour maps and other lookup tables
#
# $Revision: 1.52 $ $Date: 2024/05/02 06:19:31 $
#
colourmap <- function(col, ..., range=NULL, breaks=NULL, inputs=NULL, gamma=1) {
if(nargs() == 0) {
## null colour map
f <- lut()
} else {
## validate colour data
col2hex(col)
## store without conversion
f <- lut(col, ..., range=range, breaks=breaks, inputs=inputs, gamma=gamma)
}
class(f) <- c("colourmap", class(f))
f
}
lut <- function(outputs, ..., range=NULL, breaks=NULL, inputs=NULL, gamma=1) {
if(nargs() == 0) {
## null lookup table
f <- function(x, what="value"){NULL}
class(f) <- c("lut", class(f))
attr(f, "stuff") <- list(n=0)
return(f)
}
if(is.null(gamma)) gamma <- 1
n <- length(outputs)
given <- c(!is.null(range), !is.null(breaks), !is.null(inputs))
names(given) <- nama <- c("range", "breaks", "inputs")
ngiven <- sum(given)
if(ngiven == 0L)
stop(paste("One of the arguments",
commasep(sQuote(nama), "or"),
"should be given"))
if(ngiven > 1L) {
offending <- nama[given]
stop(paste("The arguments",
commasep(sQuote(offending)),
"are incompatible"))
}
if(!is.null(inputs)) {
#' discrete set of input values mapped to output values
if(n == 1L) {
#' constant output
n <- length(inputs)
outputs <- rep(outputs, n)
} else stopifnot(length(inputs) == length(outputs))
stuff <- list(n=n, discrete=TRUE, inputs=inputs, outputs=outputs)
f <- function(x, what="value") {
m <- match(x, stuff$inputs)
if(what == "index")
return(m)
cout <- stuff$outputs[m]
return(cout)
}
} else {
#' range of numbers, or date/time interval, mapped to colours
#' determine type of domain
timeclasses <- c("Date", "POSIXt")
is.time <- inherits(range, timeclasses) || inherits(breaks, timeclasses)
#' determine breaks
if(is.null(breaks)) {
breaks <- gammabreaks(range, n + 1L, gamma)
gamma.used <- gamma
} else {
stopifnot(length(breaks) >= 2)
if(length(outputs) == 1L) {
n <- length(breaks) - 1L
outputs <- rep(outputs, n)
} else stopifnot(length(breaks) == length(outputs) + 1L)
if(!all(diff(breaks) > 0))
stop("breaks must be increasing")
gamma.used <- NULL
}
stuff <- list(n=n, discrete=FALSE, breaks=breaks, outputs=outputs,
gamma=gamma.used)
#' use appropriate function
if(is.time) {
f <- function(x, what="value") {
x <- as.vector(as.numeric(x))
z <- findInterval(x, stuff$breaks, rightmost.closed=TRUE)
oo <- stuff$outputs
z[z <= 0 | z > length(oo)] <- NA
if(what == "index")
return(z)
cout <- oo[z]
return(cout)
}
} else {
f <- function(x, what="value") {
stopifnot(is.numeric(x))
x <- as.vector(x)
z <- findInterval(x, stuff$breaks,
rightmost.closed=TRUE)
oo <- stuff$outputs
z[z <= 0 | z > length(oo)] <- NA
if(what == "index")
return(z)
cout <- stuff$outputs[z]
return(cout)
}
}
}
attr(f, "stuff") <- stuff
class(f) <- c("lut", class(f))
f
}
print.lut <- function(x, ...) {
if(inherits(x, "colourmap")) {
tablename <- "Colour map"
outputname <- "colour"
} else {
tablename <- "Lookup table"
outputname <- "output"
}
stuff <- attr(x, "stuff")
n <- stuff$n
if(n == 0) {
## Null map
cat(paste("Null", tablename, "\n"))
return(invisible(NULL))
}
if(stuff$discrete) {
cat(paste(tablename, "for discrete set of input values\n"))
out <- data.frame(input=stuff$inputs, output=stuff$outputs)
} else {
b <- stuff$breaks
cat(paste(tablename, "for the range", prange(b[c(1L,n+1L)]), "\n"))
leftend <- rep("[", n)
rightend <- c(rep(")", n-1), "]")
inames <- paste(leftend, b[-(n+1L)], ", ", b[-1L], rightend, sep="")
out <- data.frame(interval=inames, output=stuff$outputs)
}
colnames(out)[2L] <- outputname
print(out)
if(!is.null(gamma <- stuff$gamma) && gamma != 1)
cat(paste("Generated using gamma =", gamma, "\n"))
invisible(NULL)
}
print.colourmap <- function(x, ...) {
NextMethod("print")
}
summary.lut <- function(object, ...) {
s <- attr(object, "stuff")
if(inherits(object, "colourmap")) {
s$tablename <- "Colour map"
s$outputname <- "colour"
} else {
s$tablename <- "Lookup table"
s$outputname <- "output"
}
class(s) <- "summary.lut"
return(s)
}
print.summary.lut <- function(x, ...) {
n <- x$n
if(n == 0) {
cat(paste("Null", x$tablename, "\n"))
return(invisible(NULL))
}
if(x$discrete) {
cat(paste(x$tablename, "for discrete set of input values\n"))
out <- data.frame(input=x$inputs, output=x$outputs)
} else {
b <- x$breaks
cat(paste(x$tablename, "for the range", prange(b[c(1L,n+1L)]), "\n"))
leftend <- rep("[", n)
rightend <- c(rep(")", n-1L), "]")
inames <- paste(leftend, b[-(n+1L)], ", ", b[-1L], rightend, sep="")
out <- data.frame(interval=inames, output=x$outputs)
}
colnames(out)[2L] <- x$outputname
print(out)
}
plot.colourmap <- local({
# recognised additional arguments to image.default() and axis()
imageparams <- c("main", "asp", "sub", "axes", "ann",
"cex", "font",
"cex.axis", "cex.lab", "cex.main", "cex.sub",
"col.axis", "col.lab", "col.main", "col.sub",
"font.axis", "font.lab", "font.main", "font.sub")
axisparams <- c("cex",
"cex.axis", "cex.lab",
"col.axis", "col.lab",
"font.axis", "font.lab",
"las", "mgp", "xaxp", "yaxp",
"tck", "tcl", "xpd")
linmap <- function(x, from, to) {
dFrom <- as.numeric(diff(from))
dTo <- as.numeric(diff(to))
b <- dTo/dFrom
if(is.nan(b)) b <- 0
if(!is.finite(b)) stop("Internal error: Cannot map zero width interval")
to[1L] + b * (x - from[1L])
}
ensurenumeric <- function(x) { if(is.numeric(x)) x else as.numeric(x) }
# rules to determine the ribbon dimensions when one dimension is given
widthrule <- function(heightrange, separate, n, gap) {
dh <- diff(heightrange)
if(separate || dh == 0) 1 else dh/10
}
heightrule <- function(widthrange, separate, n, gap) {
dw <- diff(widthrange)
if(dw == 0) 1 else (dw * (if(separate) (n + (n-1)*gap) else 10))
}
sideCode <- function(side) {
if(is.numeric(side)) {
stopifnot(side %in% 1:4)
sidecode <- side
} else if(is.character(side)) {
stopifnot(side %in% c("bottom", "left", "top", "right"))
sidecode <- match(side, c("bottom", "left", "top", "right"))
} else stop("Unrecognised format for 'side'")
return(sidecode)
}
Ticks <- function(usr, log=FALSE, nint=NULL, ..., clip=TRUE) {
#' modification of grDevices::axisTicks
#' constrains ticks to be inside the specified range 'usr' if clip=TRUE
#' accepts nint=NULL as if it were missing
z <- if(is.null(nint)) axisTicks(usr=usr, log=log, ...) else
axisTicks(usr=usr, log=log, nint=nint, ...)
if(clip) {
zlimits <- if(log) 10^usr else usr
z <- z[inside.range(z, zlimits)]
}
return(z)
}
plot.colourmap <- function(x, ..., main,
xlim=NULL, ylim=NULL, vertical=FALSE, axis=TRUE,
labelmap=NULL, gap=0.25, add=FALSE,
increasing=NULL, nticks=5, box=NULL) {
if(missing(main))
main <- short.deparse(substitute(x))
stuff <- attr(x, "stuff")
col <- stuff$outputs
n <- stuff$n
if(n == 0) {
## Null map
return(invisible(NULL))
}
discrete <- stuff$discrete
if(discrete) {
check.1.real(gap, "In plot.colourmap")
explain.ifnot(gap >= 0, "In plot.colourmap")
}
separate <- discrete && (gap > 0)
if(is.null(labelmap)) {
labelmap <- function(x) x
} else if(is.numeric(labelmap) && length(labelmap) == 1L && !discrete) {
labscal <- labelmap
labelmap <- function(x) { x * labscal }
} else stopifnot(is.function(labelmap))
if(is.null(increasing))
increasing <- !(discrete && vertical)
reverse <- !increasing
#' determine pixel entries 'v' and colour map breakpoints 'bks'
#' to be passed to 'image.default'
trivial <- FALSE
if(!discrete) {
# real numbers: continuous ribbon
bks <- stuff$breaks
rr <- range(bks)
trivial <- (diff(rr) == 0)
v <- if(trivial) rr[1] else
seq(from=rr[1L], to=rr[2L], length.out=max(n+1L, 1024))
} else if(!separate) {
# discrete values: blocks of colour, run together
v <- (1:n) - 0.5
bks <- 0:n
rr <- c(0,n)
} else {
# discrete values: separate blocks of colour
vleft <- (1+gap) * (0:(n-1L))
vright <- vleft + 1
v <- vleft + 0.5
rr <- c(0, n + (n-1)*gap)
}
# determine position of ribbon or blocks of colour
if(is.null(xlim) && is.null(ylim)) {
u <- widthrule(rr, separate, n, gap)
if(!vertical) {
xlim <- rr
ylim <- c(0,u)
} else {
xlim <- c(0,u)
ylim <- rr
}
} else if(is.null(ylim)) {
if(!vertical)
ylim <- c(0, widthrule(xlim, separate, n, gap))
else
ylim <- c(0, heightrule(xlim, separate, n, gap))
} else if(is.null(xlim)) {
if(!vertical)
xlim <- c(0, heightrule(ylim, separate, n, gap))
else
xlim <- c(0, widthrule(ylim, separate, n, gap))
}
# .......... initialise plot ...............................
if(!add)
do.call.matched(plot.default,
resolve.defaults(list(x=xlim, y=ylim,
type="n", main=main,
axes=FALSE, xlab="", ylab="",
asp=1.0),
list(...)))
if(separate) {
# ................ plot separate blocks of colour .................
if(reverse)
col <- rev(col)
if(!vertical) {
# horizontal arrangement of blocks
xleft <- linmap(vleft, rr, xlim)
xright <- linmap(vright, rr, xlim)
y <- ylim
z <- matrix(1, 1L, 1L)
for(i in 1:n) {
x <- c(xleft[i], xright[i])
do.call.matched(image.default,
resolve.defaults(list(x=ensurenumeric(x),
y=ensurenumeric(y),
z=z, add=TRUE),
list(...),
list(col=col[i])),
extrargs=imageparams)
}
} else {
# vertical arrangement of blocks
x <- xlim
ylow <- linmap(vleft, rr, ylim)
yupp <- linmap(vright, rr, ylim)
z <- matrix(1, 1L, 1L)
for(i in 1:n) {
y <- c(ylow[i], yupp[i])
do.call.matched(image.default,
resolve.defaults(list(x=ensurenumeric(x),
y=ensurenumeric(y),
z=z, add=TRUE),
list(...),
list(col=col[i])),
extrargs=imageparams)
}
}
} else {
# ................... plot ribbon image .............................
if(!vertical) {
# horizontal colour ribbon
x <- linmap(v, rr, xlim)
y <- ylim
z <- matrix(v, ncol=1L)
} else {
# vertical colour ribbon
y <- linmap(v, rr, ylim)
z <- matrix(v, nrow=1L)
x <- xlim
}
#' deal with Date or integer values
x <- ensurenumeric(x)
if(!trivial) {
if(any(diff(x) == 0))
x <- seq(from=x[1L], to=x[length(x)], length.out=length(x))
y <- ensurenumeric(y)
if(any(diff(y) == 0))
y <- seq(from=y[1L], to=y[length(y)], length.out=length(y))
bks <- ensurenumeric(bks)
if(any(diff(bks) <= 0)) {
ok <- (diff(bks) > 0)
bks <- bks[ok]
col <- col[ok]
}
}
if(reverse)
col <- rev(col)
do.call.matched(image.default,
resolve.defaults(list(x=x, y=y, z=z, add=TRUE),
list(...),
list(breaks=ensurenumeric(bks),
col=col)),
extrargs=imageparams)
}
#' draw box around colours?
#' default is TRUE unless drawing blocks of colour with gaps between.
if(is.null(box)) box <- !separate
if(!isFALSE(box))
rect(xlim[1], ylim[1], xlim[2], ylim[2])
if(axis) {
# ................. draw annotation ..................
if(!vertical) {
# add horizontal axis/annotation
if(discrete) {
la <- paste(labelmap(stuff$inputs))
at <- linmap(v, rr, xlim)
} else {
la <- Ticks(rr, nint=nticks)
at <- linmap(la, rr, xlim)
la <- labelmap(la)
}
if(reverse)
at <- rev(at)
# default axis position is below the ribbon (side=1)
side <- resolve.1.default("side", list(...), list(side=1L))
sidecode <- sideCode(side)
if(!(sidecode %in% c(1L,3L)))
warning(paste("side =", sidecode,
"is not consistent with horizontal orientation"))
pos <- c(ylim[1L], xlim[1L], ylim[2L], xlim[2L])[sidecode]
# don't draw axis lines if plotting separate blocks
lwd0 <- if(separate) 0 else 1
# draw axis
do.call.matched(graphics::axis,
resolve.defaults(list(...),
list(side = 1L, pos = pos,
at = ensurenumeric(at)),
list(labels=la, lwd=lwd0)),
extrargs=axisparams)
} else {
# add vertical axis
if(discrete) {
la <- paste(labelmap(stuff$inputs))
at <- linmap(v, rr, ylim)
} else {
la <- Ticks(rr, nint=nticks)
at <- linmap(la, rr, ylim)
la <- labelmap(la)
}
if(reverse)
at <- rev(at)
# default axis position is to the right of ribbon (side=4)
side <- resolve.1.default("side", list(...), list(side=4))
sidecode <- sideCode(side)
if(!(sidecode %in% c(2L,4L)))
warning(paste("side =", sidecode,
"is not consistent with vertical orientation"))
pos <- c(ylim[1L], xlim[1L], ylim[2L], xlim[2L])[sidecode]
# don't draw axis lines if plotting separate blocks
lwd0 <- if(separate) 0 else 1
# draw labels horizontally if plotting separate blocks
las0 <- if(separate) 1 else 0
# draw axis
do.call.matched(graphics::axis,
resolve.defaults(list(...),
list(side=4, pos=pos,
at=ensurenumeric(at)),
list(labels=la, lwd=lwd0, las=las0)),
extrargs=axisparams)
}
}
invisible(NULL)
}
plot.colourmap
})
# Interpolate a colourmap or lookup table defined on real numbers
interp.colourmap <- function(m, n=512) {
if(!inherits(m, "colourmap"))
stop("m should be a colourmap")
st <- attr(m, "stuff")
if(st$discrete) {
# discrete set of input values mapped to colours
xknots <- st$inputs
# Ensure the inputs are real numbers
if(!is.numeric(xknots))
stop("Cannot interpolate: inputs are not numerical values")
} else {
# interval of real line, chopped into intervals, mapped to colours
# Find midpoints of intervals
bks <- st$breaks
nb <- length(bks)
xknots <- (bks[-1L] + bks[-nb])/2
}
# corresponding colours in hsv coordinates
yknots.hsv <- rgb2hsva(col2rgb(st$outputs, alpha=TRUE))
# transform 'hue' from polar to cartesian coordinate
# divide domain into n equal intervals
xrange <- range(xknots)
xbreaks <- seq(xrange[1L], xrange[2L], length=n+1L)
xx <- (xbreaks[-1L] + xbreaks[-(n+1L)])/2
# interpolate saturation and value in hsv coordinates
yy.sat <- approx(x=xknots, y=yknots.hsv["s", ], xout=xx)$y
yy.val <- approx(x=xknots, y=yknots.hsv["v", ], xout=xx)$y
# interpolate hue by first transforming polar to cartesian coordinate
yknots.hue <- 2 * pi * yknots.hsv["h", ]
yy.huex <- approx(x=xknots, y=cos(yknots.hue), xout=xx)$y
yy.huey <- approx(x=xknots, y=sin(yknots.hue), xout=xx)$y
yy.hue <- (atan2(yy.huey, yy.huex)/(2 * pi)) %% 1
# handle transparency
yknots.alpha <- yknots.hsv["alpha", ]
if(all(yknots.alpha == 1)) {
## opaque colours: form using hue, sat, val
yy <- hsv(yy.hue, yy.sat, yy.val)
} else {
## transparent colours: interpolate alpha
yy.alpha <- approx(x=xknots, y=yknots.alpha, xout=xx)$y
## form colours using hue, sat, val, alpha
yy <- hsv(yy.hue, yy.sat, yy.val, yy.alpha)
}
# done
f <- colourmap(yy, breaks=xbreaks)
return(f)
}
interp.colours <- function(x, length.out=512) {
y <- colourmap(x, range=c(0,1))
z <- interp.colourmap(y, length.out)
oo <- attr(z, "stuff")$outputs
return(oo)
}
tweak.colourmap <- local({
is.hex <- function(z) {
is.character(z) &&
all(nchar(z, keepNA=TRUE) %in% c(7L,9L)) &&
identical(substr(z, 1L, 7L), substr(col2hex(z), 1L, 7L))
}
tweak.colourmap <- function(m, col, ..., inputs=NULL, range=NULL) {
if(!inherits(m, "colourmap"))
stop("m should be a colourmap")
if(is.null(inputs) && is.null(range))
stop("Specify either inputs or range")
if(!is.null(inputs) && !is.null(range))
stop("Do not specify both inputs and range")
## determine indices of colours to be changed
if(!is.null(inputs)) {
ix <- m(inputs, what="index")
} else {
if(!(is.numeric(range) && length(range) == 2 && diff(range) > 0))
stop("range should be a numeric vector of length 2 giving (min, max)")
if(length(col2hex(col)) != 1L)
stop("When range is given, col should be a single colour value")
ixr <- m(range, what="index")
ix <- (ixr[1L]):(ixr[2L])
}
## reassign colours
st <- attr(m, "stuff")
outputs <- st$outputs
result.hex <- FALSE
if(is.hex(outputs)) {
## convert replacement data to hex
col <- col2hex(col)
result.hex <- TRUE
} else if(is.hex(col)) {
## convert existing data to hex
outputs <- col2hex(outputs)
result.hex <- TRUE
} else if(!(is.character(outputs) && is.character(col))) {
## unrecognised format - convert both to hex
outputs <- col2hex(outputs)
col <- col2hex(col)
result.hex <- TRUE
}
if(result.hex) {
## hex codes may be 7 or 9 characters
outlen <- nchar(outputs)
collen <- nchar(col)
if(length(unique(c(outlen, collen))) > 1L) {
## convert all to 9 characters
if(any(bad <- (outlen == 7)))
outputs[bad] <- paste0(outputs[bad], "FF")
if(any(bad <- (collen == 7)))
col[bad] <- paste0(col[bad], "FF")
}
}
## Finally, replace
outputs[ix] <- col
st$outputs <- outputs
attr(m, "stuff") <- st
assign("stuff", st, envir=environment(m))
return(m)
}
tweak.colourmap
})
colouroutputs <- function(x) {
stopifnot(inherits(x, "colourmap"))
attr(x, "stuff")$outputs
}
"colouroutputs<-" <- function(x, value) {
stopifnot(inherits(x, "colourmap"))
st <- attr(x, "stuff")
col2hex(value) # validates colours
st$outputs[] <- value
attr(x, "stuff") <- st
assign("stuff", st, envir=environment(x))
return(x)
}
restrict.colourmap <- function(x, ..., range=NULL, breaks=NULL, inputs=NULL) {
stopifnot(inherits(x, "colourmap"))
given <- c(!is.null(range), !is.null(breaks), !is.null(inputs))
names(given) <- nama <- c("range", "breaks", "inputs")
ngiven <- sum(given)
if(ngiven == 0L)
return(x)
if(ngiven > 1L) {
offending <- nama[given]
stop(paste("The arguments",
commasep(sQuote(offending)),
"are incompatible"))
}
stuff <- attr(x, "stuff")
if(!is.null(inputs)) {
## discrete colour map
if(!stuff$discrete)
stop("Cannot update 'inputs'; the existing colour map is not discrete",
call.=FALSE)
oldinputs <- stuff$inputs
oldoutputs <- stuff$outputs
m <- match(inputs, oldinputs)
if(any(is.na(m)))
stop("New inputs are not a subset of the old inputs", call.=FALSE)
result <- colourmap(oldoutputs[m], inputs=inputs)
} else if(!is.null(range)) {
## colour map for continuous domain
## range specified
if(stuff$discrete)
stop("Cannot update 'range'; the existing colour map is discrete",
call.=FALSE)
check.range(range)
oldbreaks <- stuff$breaks
if(!all(inside.range(range, range(oldbreaks))))
stop("new range of values is not a subset of current range")
## restrict existing breaks to new range
newbreaks <- pmax(range[1], pmin(range[2], oldbreaks))
newbreaks <- unique(newbreaks)
## evaluate current colour at midpoint of each new interval
newmid <- newbreaks[-length(newbreaks)] + diff(newbreaks)/2
newout <- x(newmid)
result <- colourmap(newout, breaks=newbreaks)
} else {
## colour map for continuous domain
## breaks specified
if(stuff$discrete)
stop("Cannot update 'breaks'; the existing colour map is discrete",
call.=FALSE)
oldbreaks <- stuff$breaks
if(!all(inside.range(breaks, range(oldbreaks))))
stop("new range of 'breaks' is not a subset of current range of 'breaks'",
call.=FALSE)
newmid <- breaks[-length(breaks)] + diff(breaks)/2
newout <- x(newmid)
result <- colourmap(newout, breaks=breaks)
}
return(result)
}
as.colourmap <- function(x, ...) {
UseMethod("as.colourmap")
}
as.colourmap.colourmap <- function(x, ...) { x }
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.