Code file

The code for the mvtsplot function is contained in the file mvtsplot.R which is also listed in its entirety below. You can source the file directly into R by calling

    source("http://www.biostat.jhsph.edu/~rpeng/RR/mvtsplot/mvtsplot.R")
    

The code is licensed under the GNU General Public License version 2, or any later version. There is a git repository hosting the latest version of the code. If you would like to make changes, please feel free to clone the repository and send me your patches.

There is also a short article published in the Journal of Statistical Software that describes the function and provides some examples of its usage. The examples below are taken from the article; code for reproducing the examples is available on the Journal's website.

Examples

Code listing

################################################################################
## Copyright (C) 2008 Roger D. Peng 
## 
## 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.
## 
## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
## 02110-1301, USA
################################################################################

library(splines)


mvtsplot <- local({
        drawImage <- function(cx, pal, nlevels, xlim, cn, xtime, group, gcol) {
                par(las = 1, cex.axis = 0.6)
                cn <- colnames(cx)
                nc <- ncol(cx)
                side2 <- 0.2

                ## Setup image plot
                if(!is.null(cn)) 
                        side2 <- max(side2, max(strwidth(cn, "inches")) + 0.1)
                else
                        cn <- as.character(1, nc)
                par(mai = c(0.4, side2, 0.1, 0.1))
                image(unclass(xtime), seq_len(nc), cx, col = pal(nlevels),
                      xlim = xlim, xaxt = "n", yaxt = "n", ylab = "", xlab = "")
                axis(2, at = seq_len(nc), cn)
                Axis(xtime, side = 1)
                box()

                if(!is.null(group)) {
                        usrpar <- par("usr")
                        par(usr = c(usrpar[1:2], 0, 1))
                        tg <- table(group)[-nlevels(group)]
                        abline(h = cumsum(tg) / nc, lwd = 2, col = gcol)
                }
        }

        drawImageMargin <- function(cx, pal, nlevels, xlim, cn, xtime, group,
                                    gcol, smooth.df, rowm, nr, bottom.ylim, colm, right.xlim,
                                    main) {
                op <- par(no.readonly = TRUE)
                on.exit(par(op))
                par(las = 1, cex.axis = 0.6)

                cn <- colnames(cx)
                nc <- ncol(cx)
                side2 <- 0.55
                utime <- unclass(xtime)

                layout(rbind(c(1, 1, 1, 1, 1, 1, 3),
                             c(1, 1, 1, 1, 1, 1, 3),
                             c(1, 1, 1, 1, 1, 1, 3),
                             c(2, 2, 2, 2, 2, 2, 4)))

                ## Setup image plot
                if(!is.null(cn))
                        side2 <- max(0.55, max(strwidth(cn, "inches")) + 0.1)
                else
                        cn <- rep("", nc)
                par(mai = c(0.05, side2, 0.1, 0.05))
                image(utime, seq_len(nc), cx, col = pal(nlevels),
                      xlim = xlim, xaxt = "n", yaxt = "n", ylab = "", xlab = "")
                axis(2, at = seq_len(nc), cn)
                box()

                if(!is.null(group)) {
                        usrpar <- par("usr")
                        par(usr = c(usrpar[1:2], 0, 1))
                        tg <- table(group)[-nlevels(group)]
                        abline(h = cumsum(tg) / nc, lwd = 2, col = gcol)
                }
                ## Plot bottom
                if(!is.null(smooth.df)) {  ## Smooth row stats
                        xx <- seq_along(rowm)
                        tmp.fit <- lm(rowm ~ ns(xx, smooth.df),
                                      na.action = na.exclude)
                        rowm <- predict(tmp.fit)
                }
                bottom.ylim <- if(is.null(bottom.ylim))
                        range(rowm, na.rm = TRUE)
                else
                        bottom.ylim
                par(mai = c(0.4, side2, 0.05, 0.05))
                plot(utime, rep(0, nr), type = "n",
                     ylim = bottom.ylim, xaxt = "n", xlab = "", ylab = "Level")
                par(usr = c(xlim, par("usr")[3:4]))
                nalines(utime, rowm)
                Axis(xtime, side = 1)

                ## Plot right side
                right.xlim <- if(is.null(right.xlim))
                        range(colm, na.rm = TRUE)
                else
                        right.xlim
                par(mai = c(0.05, 0.05, 0.1, 0.1))
                plot(colm[3,], 1:nc, type = "n", ylab = "", yaxt = "n",
                     xlab = "", xlim = right.xlim)

                usrpar <- par("usr")
                par(usr = c(usrpar[1:2], 0, 1))
                ypos <- (1:nc - 1 / 2) / nc

                segments(colm[1, ], ypos, colm[2, ], ypos, col = gray(0.6))
                segments(colm[4, ], ypos, colm[5, ], ypos, col = gray(0.6))
                points(colm[3,], ypos, pch = 19, cex = 0.6)

                if(!is.null(group))
                        abline(h = cumsum(tg) / nc, lwd = 2, col = gcol)

                ## Plot lower right
                blankplot()
                text(0, 0, main)

                rval <- list(z = cx, rowm = rowm, colm = colm)
                invisible(rval)
        }

        blankplot <- function() {
                plot(0, 0, xlab = "", ylab = "", axes = FALSE, type = "n")
        }

        ## Discretize columns of a matrix in to categories defined by 'levels'

        catcols <- function(x, levels, norm) {
                if(norm == "internal") {
                        ## Each column gets its own set of categories
                        apply(x, 2, function(y) categorize(y, levels))
                }
                else {
                        ## Use a 'global' set of categories
                        xv <- as.vector(x)
                        y <- categorize(xv, levels)
                        matrix(y, nrow = nrow(x), ncol = ncol(x))
                }
        }

        ## Discretize a vector 'x' into categories defined by 'levels'.

        categorize <- function(x, levels, jitter = TRUE) {
                ## 'x' is a vector; 'levels' is a single integer, or a vector
                ## of quantiles
                if(length(levels) == 1)
                        levels <- seq(0, 1, len = levels + 1)
                qq <- quantile(x, levels, na.rm = TRUE)
                qqu <- unique(qq)

                if(length(qqu) != length(qq)) {
                        if(!jitter)
                                return(rep(NA, length(x)))
                        qqu <- try(suppressWarnings({
                                x <- jitter(x)
                                qq <- quantile(x, levels, na.rm = TRUE)
                                unique(qq)
                        }), silent = TRUE)
                        if(inherits(qqu, "try-error") || length(qqu) != length(qq))
                                return(rep(NA, length(x)))
                }
                cx <- cut(x, qqu, include.lowest = TRUE)
                as.numeric(unclass(cx))
        }

        smoothX <- function(x, df) {
                apply(x, 2, function(v) splineFillIn(v, df))
        }

        reorderCols <- function(x, group) {
                if(length(group) != ncol(x))
                        stop("'group' vector should equal 'ncol(x)'")
                idx.split <- split(seq_len(ncol(x)), group)
                idx.reordered <- unlist(idx.split)
                x[, idx.reordered, drop = FALSE]
        }

        rangeby <- function(x, f) {
                use <- complete.cases(x, f)
                range(x[use])
        }

        splineFillIn <- function(x, df) {
                if(all(is.na(x)))
                        return(x)
                tt <- seq_along(x)
                rng <- rangeby(tt, x)
                fit <- lm(x ~ ns(tt, df), na.action = na.exclude)
                idx <- seq(rng[1], rng[2])
                x[idx] <- suppressWarnings({
                        predict(fit, data.frame(tt = idx))
                })
                x
        }

        sumNA <- function(x) {
                if(all(is.na(x)))
                        NA
                else
                        sum(x, na.rm = TRUE)
        }

        checkMatrix <- function(x) {
                if(!is.matrix(x))
                        stop("'x' should be a matrix")
                if(ncol(x) < 2)
                        stop("'x' should have more than 1 column")
                if(nrow(x) < 2)
                        stop("'x' should have more than 1 row")
                TRUE
        }

        ## This function is like 'lines' in that it draws lines between
        ## points.  For two points that are consecutive, the line is drawn
        ## "black".  If one or more missing values is between two points, then
        ## the line is drawn grey (or 'NAcol').

        nalines <- function(x, y, NAcol = gray(0.6), lattice = FALSE, ...) {
                use <- complete.cases(x, y)
                idx <- which(use)
                n <- length(idx)

                if(n < 2)
                        return(invisible())
                for(i in seq_len(n - 1)) {
                        j <- idx[i]
                        k <- idx[i+1]
                        col <- if((k - j) > 1)
                                NAcol
                        else
                                "black"
                        if(lattice)
                                llines(c(x[j], x[k]), c(y[j], y[k]), col = col)
                        else
                                lines(c(x[j], x[k]), c(y[j], y[k]), col = col)
                }
                invisible()
        }


        function(x, group = NULL, xtime = NULL,
                 norm = c("internal", "global"), levels = 3,
                 smooth.df = NULL, margin = TRUE,
                 sort = NULL,
                 main = "", palette = "PRGn",
                 rowstat = "median",
                 xlim, bottom.ylim = NULL,
                 right.xlim = NULL,
                 gcol = 1) {
                if(is.data.frame(x))
                        x <- data.matrix(x)
                checkMatrix(x)
                norm <- match.arg(norm)

                if(!is.null(sort))
                        sort <- match.fun(sort)
                rowstat <- match.fun(rowstat)

                if(!require(RColorBrewer))
                        stop("'RColorBrewer' package required")
                if(is.null(xtime)) {
                        xtime <- seq_len(nrow(x))
                        xlim <- c(0, max(xtime))
                }
                else
                        xlim <- range(xtime)
                if(!is.null(group)) {
                        group <- as.factor(group)
                        x <- reorderCols(x, group)
                }
                if(!margin && !is.null(sort)) {
                        stat <- apply(x, 2, sort, na.rm = TRUE)
                        x <- x[, order(stat)]
                }
                if(margin) {
                        colm <- apply(x, 2, function(x) {
                                grDevices::boxplot.stats(x)$stats
                        })
                        if(!is.null(sort)) {
                                stat <- apply(x, 2, sort, na.rm = TRUE)
                                ord <- order(stat)
                                x <- x[, ord]
                                colm <- colm[, ord]
                        }
                }
                if(is.null(smooth.df))
                        cx <- catcols(x, levels, norm)
                else {
                        x <- smoothX(x, smooth.df)
                        cx <- catcols(x, levels, norm)
                }
                if(margin)
                        rowm <- apply(x, 1, rowstat, na.rm = TRUE)
                colnames(cx) <- colnames(x)
                empty <- apply(cx, 2, function(x) all(is.na(x)))

                if(any(empty)) {  ## Remove empty columns
                        cx <- cx[, !empty]

                        if(margin)
                                colm <- colm[, !empty]
                }
                pal <- colorRampPalette(brewer.pal(4, palette))

                nlevels <- if(length(levels) == 1)
                        levels
                else
                        length(levels)
                if(margin)
                        drawImageMargin(cx, pal, nlevels, xlim, cn, xtime,
                                        group, gcol, smooth.df, rowm, nrow(x),
                                        bottom.ylim, colm, right.xlim, main)
                else
                        drawImage(cx, pal, nlevels, xlim, cn, xtime, group, gcol)
        }

})