blob: 74ba53c495e75510953195cc77791f7a0268edb8 [file] [log] [blame]
# File src/library/graphics/R/fourfoldplot.R
# Part of the R package, https://www.R-project.org
#
# Copyright (C) 1995-2016 The R Core Team
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# A copy of the GNU General Public License is available at
# https://www.R-project.org/Licenses/
fourfoldplot <-
function(x, color = c("#99CCFF", "#6699CC"), conf.level = 0.95,
std = c("margins", "ind.max", "all.max"), margin = c(1, 2),
space = 0.2, main = NULL, mfrow = NULL, mfcol = NULL)
{
## Code for producing fourfold displays.
## Reference:
## Friendly, M. (1994).
## A fourfold display for 2 by 2 by \eqn{k} tables.
## Technical Report 217, York University, Psychology Department.
## http://www.math.yorku.ca/SCS/Papers/4fold/4fold.ps.gz
##
## Implementation notes:
##
## We need plots with aspect ratio FIXED to 1 and glued together.
## Hence, even if k > 1 we prefer keeping everything in one plot
## region rather than using a multiple figure layout.
## Each 2 by 2 pie is is drawn into a square with x/y coordinates
## between -1 and 1, with row and column labels in [-1-space, -1]
## and [1, 1+space], respectively. If k > 1, strata labels are in
## an area with y coordinates in [1+space, 1+(1+gamma)*space],
## where currently gamma=1.25. The pies are arranged in an nr by
## nc layout, with horizontal and vertical distances between them
## set to space.
##
## The drawing code first computes the complete are of the form
## [0, totalWidth] x [0, totalHeight]
## needed and sets the world coordinates using plot.window().
## Then, the strata are looped over, and the corresponding pies
## added by filling rows or columns of the layout as specified by
## the mfrow or mfcol arguments. The world coordinates are reset
## in each step by shifting the origin so that we can always plot
## as detailed above.
if(!is.array(x))
stop("'x' must be an array")
if(length(dim(x)) == 2L) {
x <- if(is.null(dimnames(x)))
array(x, c(dim(x), 1L))
else
array(x, c(dim(x), 1L), c(dimnames(x), list(NULL)))
}
if(length(dim(x)) != 3L)
stop("'x' must be 2- or 3-dimensional")
if(any(dim(x)[1L:2L] != 2L))
stop("table for each stratum must be 2 by 2")
dnx <- dimnames(x)
if(is.null(dnx))
dnx <- vector("list", 3L)
for(i in which(sapply(dnx, is.null)))
dnx[[i]] <- LETTERS[seq_len(dim(x)[i])]
if(is.null(names(dnx)))
i <- 1L : 3L
else
i <- which(is.null(names(dnx)))
if(any(i))
names(dnx)[i] <- c("Row", "Col", "Strata")[i]
dimnames(x) <- dnx
k <- dim(x)[3L]
if(!((length(conf.level) == 1) && is.finite(conf.level) &&
(conf.level >= 0) && (conf.level < 1)))
stop("'conf.level' must be a single number between 0 and 1")
if(conf.level == 0)
conf.level <- FALSE
std <- match.arg(std)
findTableWithOAM <- function(or, tab) {
## Find a 2x2 table with given odds ratio 'or' and the margins
## of a given 2x2 table 'tab'.
m <- rowSums(tab)[1L]
n <- rowSums(tab)[2L]
t <- colSums(tab)[1L]
if(or == 1)
x <- t * n / (m + n)
else if(or == Inf)
x <- max(0, t - m)
else {
A <- or - 1
B <- or * (m - t) + (n + t)
C <- - t * n
x <- (- B + sqrt(B ^ 2 - 4 * A * C)) / (2 * A)
}
matrix(c(t - x, x, m - t + x, n - x), nrow = 2)
}
drawPie <- function(r, from, to, n = 500, col = NA) {
p <- 2 * pi * seq.int(from, to, length.out = n) / 360
x <- c(cos(p), 0) * r
y <- c(sin(p), 0) * r
polygon(x, y, col = col)
invisible(NULL)
}
stdize <- function(tab, std, x) {
## Standardize the 2 x 2 table 'tab'.
if(std == "margins") {
if(all(sort(margin) == c(1L, 2L))) {
## standardize to equal row and col margins
u <- sqrt(odds(tab)$or)
u <- u / (1 + u)
y <- matrix(c(u, 1 - u, 1 - u, u), nrow = 2L)
}
else if(margin %in% c(1, 2))
y <- prop.table(tab, margin)
else
stop("incorrect 'margin' specification")
}
else if(std == "ind.max")
y <- tab / max(tab)
else if(std == "all.max")
y <- tab / max(x)
y
}
odds <- function(x) {
## Given a 2 x 2 or 2 x 2 x k table 'x', return a list with
## components 'or' and 'se' giving the odds ratios and standard
## deviations of the log odds ratios.
if(length(dim(x)) == 2L) {
dim(x) <- c(dim(x), 1L)
k <- 1
}
else
k <- dim(x)[3L]
or <- double(k)
se <- double(k)
for(i in 1 : k) {
f <- x[ , , i]
storage.mode(f) <- "double" # protect against integer overflow
if(any(f == 0))
f <- f + 0.5
or[i] <- (f[1L, 1L] * f[2L, 2L]) / (f[1L, 2L] * f[2L, 1L])
se[i] <- sqrt(sum(1 / f))
}
list(or = or, se = se)
}
gamma <- 1.25 # Scale factor for strata labels
debug <- FALSE # Visualize the geometry.
# Not settable by user!
angle.f <- c( 90, 180, 0, 270) # 'f' for 'from'
angle.t <- c(180, 270, 90, 360) # 't' for 'to'
opar <- par(mar = c(0, 0, if(is.null(main)) 0 else 2.5, 0))
on.exit(par(opar))
byrow <- FALSE
if(!is.null(mfrow)) {
nr <- mfrow[1L]
nc <- mfrow[2L]
}
else if(!is.null(mfcol)) {
nr <- mfcol[1L]
nc <- mfcol[2L]
byrow <- TRUE
}
else {
nr <- ceiling(sqrt(k))
nc <- ceiling(k / nr)
}
if(nr * nc < k)
stop("incorrect geometry specification")
if(byrow)
indexMatrix <- expand.grid(1 : nc, 1 : nr)[, c(2, 1)]
else
indexMatrix <- expand.grid(1 : nr, 1 : nc)
totalWidth <- nc * 2 * (1 + space) + (nc - 1L) * space
totalHeight <- if(k == 1)
2 * (1 + space)
else
nr * (2 + (2 + gamma) * space) + (nr - 1L) * space
xlim <- c(0, totalWidth)
ylim <- c(0, totalHeight)
dev.hold(); on.exit(dev.flush(), add = TRUE)
plot.new()
plot.window(xlim = xlim, ylim = ylim, asp = 1)
o <- odds(x)
scale <- space / (2 * strheight("Ag"))
v <- 0.95 - max(strwidth(as.character(c(x)), cex = scale)) / 2
for(i in 1 : k) {
tab <- x[ , , i]
fit <- stdize(tab, std, x)
xInd <- indexMatrix[i, 2L]
xOrig <- 2 * xInd - 1 + (3 * xInd - 2) * space
yInd <- indexMatrix[i, 1L]
yOrig <- if(k == 1)
(1 + space)
else
(totalHeight
- (2 * yInd - 1 + ((3 + gamma) * yInd - 2) * space))
plot.window(xlim - xOrig, ylim - yOrig, asp = 1)
if(debug) {
abline(h = -1 - space)
abline(h = 1 + space)
abline(h = 1 + (1 + gamma) * space)
abline(v = -1 - space)
abline(v = 1 + space)
}
## drawLabels()
u <- 1 + space / 2
adjCorr <- 0.2
text(0, u,
paste(names(dimnames(x))[1L],
dimnames(x)[[1L]][1L],
sep = ": "),
adj = c(0.5, 0.5 - adjCorr),
cex = scale)
text(-u, 0,
paste(names(dimnames(x))[2L],
dimnames(x)[[2L]][1L],
sep = ": "),
adj = c(0.5, 0.5 - adjCorr),
cex = scale,
srt = 90)
text(0, -u,
paste(names(dimnames(x))[1L],
dimnames(x)[[1L]][2L],
sep = ": "),
adj = c(0.5, 0.5 + adjCorr),
cex = scale)
text(u, 0,
paste(names(dimnames(x))[2L],
dimnames(x)[[2L]][2L],
sep = ": "),
adj = c(0.5, 0.5 + adjCorr),
cex = scale,
srt = 90)
if(k > 1) {
text(0, 1 + (1 + gamma / 2) * space,
paste(names(dimnames(x))[3L],
dimnames(x)[[3L]][i],
sep = ": "),
cex = gamma * scale)
}
## drawFrequencies()
d <- odds(tab)$or
drawPie(sqrt(fit[1,1]), 90, 180, col = color[1 + (d > 1)])
drawPie(sqrt(fit[2,1]), 180, 270, col = color[2 - (d > 1)])
drawPie(sqrt(fit[1,2]), 0, 90, col = color[2 - (d > 1)])
drawPie(sqrt(fit[2,2]), 270, 360, col = color[1 + (d > 1)])
u <- 1 - space / 2
text(c(-v, -v, v, v),
c( u, -u, u, -u),
as.character(c(tab)),
cex = scale)
## drawConfBands()
if(is.numeric(conf.level)) {
or <- o$or[i]
se <- o$se[i]
## lower
theta <- or * exp(stats::qnorm((1 - conf.level) / 2) * se)
tau <- findTableWithOAM(theta, tab)
r <- sqrt(c(stdize(tau, std, x)))
for(j in 1 : 4)
drawPie(r[j], angle.f[j], angle.t[j])
## upper
theta <- or * exp(stats::qnorm((1 + conf.level) / 2) * se)
tau <- findTableWithOAM(theta, tab)
r <- sqrt(c(stdize(tau, std, x)))
for(j in 1 : 4)
drawPie(r[j], angle.f[j], angle.t[j])
}
## drawBoxes()
polygon(c(-1, 1, 1, -1),
c(-1, -1, 1, 1))
lines(c(-1, 1), c(0, 0))
for(j in seq.int(from = -0.8, to = 0.8, by = 0.2))
lines(c(j, j), c(-0.02, 0.02))
for(j in seq.int(from = -0.9, to = 0.9, by = 0.2))
lines(c(j, j), c(-0.01, 0.01))
lines(c(0, 0), c(-1, 1))
for(j in seq.int(from = -0.8, to = 0.8, by = 0.2))
lines(c(-0.02, 0.02), c(j, j))
for(j in seq.int(from = -0.9, to = 0.9, by = 0.2))
lines(c(-0.01, 0.01), c(j, j))
}
if(!is.null(main))
mtext(main, cex = 1.5, adj = 0.5)
return(invisible())
}