R/heplot3d.R

Defines functions heplot3d

Documented in heplot3d

# last modified 23 January 2007 by J. Fox
# last modified 10/6/2008 8:32AM by M. Friendly
#    - added shade=, shade.alpha=, wire=
#    - fixed: grand.mean=FALSE not respected
#    - replaced sphere at grand.mean with cross3d
# last modified 10/7/2008 11:26AM by M. Friendly
#    - color means according to the color of the term
# last modified 10/15/2008 4:21PM by M. Friendly
#    - return bounding boxes of the ellipsoids
# last modified 10/28/2008 9:37AM by M. Friendly
#    - replaced rgl.texts with texts3d
#    - replaced ellipsoid() with spheres3d() for plotting means
# last modified 11/4/2008 12:50PM by M. Friendly
#    - reverted to ellipsoid() for plotting means
# last modified 11/6/2008 by M. Friendly
#    - added xlim, ylim, zlim arguments to allow expanding the plot bbox (for candisc)
# last modified 29 Dec 2009 by M. Friendly -- added idate=, idesign=, icontrasts, iterm for repeated measures
# last modified 30 Dec 2009 by M. Friendly -- debugged repeated measures
# last modified  1 Jan 2010 by M. Friendly -- debugged repeated measures again
# last modified  1 Jan 2010 by M. Friendly -- merged heplot3d.R and heplot.mlm.R
# last modified 12 Feb 2010 by M. Friendly -- fixed buglet with text3d causing rgl to crash (thx: Duncan Murdoch)
# last modified 23 Jul 2010 by M. Friendly -- return radius
# -- added err.label to allow changing label for Error ellipse
# last modified 26 Apr 2013 by M. Friendly 
# -- modified ellipsoid to reduce striation (Thx: Duncan Murdoch)
# -- changed default colors and default fill.alpha
# 10/20/2020 Fixed moire problem in heplot3d when dfh <3 (Thx: Duncan Murdoch)
# -- uses back="culled" & depth_mask=FALSE properties
# 9/22/2022  Add cex.label arg

# TODO what is this doing here?
savedvars <- new.env(parent=emptyenv())



#' Three-Dimensional HE Plots
#' 
#' This function plots ellipsoids in 3D representing the hypothesis and error
#' sums-of-squares-and-products matrices for terms and linear hypotheses in a
#' multivariate linear model.
#' 
#' When the H matrix for a term has rank < 3, the ellipsoid collapses to an
#' ellipse (rank(H)=2) or a line (rank(H)=1).
#' 
#' Rotating the plot can be particularly revealing, showing views in which H
#' variation is particularly large or small in relation to E variation.  See
#' \code{\link[rgl]{play3d}} and \code{\link[rgl]{movie3d}} for details on
#' creating animations.
#' 
#' The arguments \code{xlim}, \code{ylim}, and \code{zlim} can be used to
#' expand the bounding box of the axes, but cannot decrease it.
#' 
#' @aliases heplot3d heplot3d.mlm
#' @param mod a model object of class \code{"mlm"}.
#' @param terms a logical value or character vector of terms in the model for
#'        which to plot hypothesis matrices; if missing or \code{TRUE}, defaults to
#'        all terms; if \code{FALSE}, no terms are plotted.
#' @param hypotheses optional list of linear hypotheses for which to plot
#'        hypothesis matrices; hypotheses are specified as for the
#'        \code{\link[car]{linearHypothesis}} function in the \code{car} package; the
#'        list elements can be named, in which case the names are used.
#' @param term.labels logical value or character vector of names for the terms
#'        to be plotted. If \code{TRUE} (the default) the names of the terms are used;
#'        if \code{FALSE}, term labels are not plotted.
#' @param hyp.labels logical value or character vector of names for the
#'        hypotheses to be plotted. If \code{TRUE} (the default) the names of
#'        components of the list of hypotheses are used; if \code{FALSE}, hypothesis
#'        labels are not plotted.
#' @param err.label Label for the error ellipse
#' @param variables indices or names of the three response variables to be
#'        plotted; defaults to \code{1:3}.
#' @param error.ellipsoid if \code{TRUE}, plot the error ellipsoid; defaults to
#'        \code{TRUE}, if the argument \code{add} is \code{FALSE} (see below).
#' @param factor.means logical value or character vector of names of factors
#'        for which the means are to be plotted, or \code{TRUE} or \code{FALSE};
#'        defaults to \code{TRUE}, if the argument \code{add} is \code{FALSE} (see
#'        below).
#' @param grand.mean if \code{TRUE}, plot the centroid for all of the data;
#'        defaults to \code{TRUE}, if the argument \code{add} is \code{FALSE} (see
#'        below).
#' @param remove.intercept if \code{TRUE} (the default), do not plot the
#'        ellipsoid for the intercept even if it is in the MANOVA table.
#' @param type ``type'' of sum-of-squares-and-products matrices to compute; one
#'        of \code{"II"}, \code{"III"}, \code{"2"}, or \code{"3"}, where \code{"II"}
#'        is the default (and \code{"2"} is a synonym).
#' @param idata an optional data frame giving a factor or factors defining the
#'        intra-subject model for multivariate repeated-measures data.  See Details of
#'        \code{\link[car]{Anova}} for an explanation of the intra-subject design and
#'        for further explanation of the other arguments relating to intra-subject
#'        factors.
#' @param idesign a one-sided model formula using the ``data'' in idata and
#'        specifying the intra-subject design for repeated measure models.
#' @param icontrasts names of contrast-generating functions to be applied by
#'        default to factors and ordered factors, respectively, in the within-subject
#'        ``data''; the contrasts must produce an intra-subject model matrix in which
#'        different terms are orthogonal. The default is c("contr.sum", "contr.poly").
#' @param imatrix In lieu of \code{idata} and \code{idesign}, you can specify
#'        the intra-subject design matrix directly via \code{imatrix}, in the form of
#'        list of named elements.  Each element gives the columns of the
#'        within-subject model matrix for an intra-subject term to be tested, and must
#'        have as many rows as there are responses; the columns of the within-subject
#'        model matrix for \emph{different} terms must be mutually orthogonal.
#' @param iterm For repeated measures designs, you must specify one
#'        intra-subject term (a character string) to select the SSPE (E) matrix used
#'        in the HE plot.  Hypothesis terms plotted include the \code{iterm} effect as
#'        well as all interactions of \code{iterm} with \code{terms}.
#' @param manova optional \code{Anova.mlm} object for the model; if absent a
#'        MANOVA is computed. Specifying the argument can therefore save computation
#'        in repeated calls.
#' @param size how to scale the hypothesis ellipsoid relative to the error
#'        ellipsoid; if \code{"evidence"}, the default, the scaling is done so that a
#'        ``significant'' hypothesis ellipsoid extends outside of the error ellipsoid;
#'        if \code{"effect.size"}, the hypothesis ellipsoid is on the same scale as
#'        the error ellipsoid.
#' @param level equivalent coverage of ellipsoid for normally-distributed
#'        errors, defaults to \code{0.68}.
#' @param alpha significance level for Roy's greatest-root test statistic; if
#'        \code{size="evidence"}, then the hypothesis ellipsoid is scaled so that it
#'        just touches the error ellipsoid at the specified alpha level; a larger
#'        hypothesis ellipsoid therefore indicates statistical significance; defaults
#'        to \code{0.05}.
#' @param segments number of segments composing each ellipsoid; defaults to \code{40}.
#' @param col a color or vector of colors to use in plotting ellipsoids; the
#'        first color is used for the error ellipsoid; the remaining colors ---
#'        recycled as necessary --- are used for the hypothesis ellipsoid.  A single
#'        color can be given, in which case it is used for all ellipsoid.  For
#'        convenience, the default colors for all heplots produced in a given session
#'        can be changed by assigning a color vector via \code{options(heplot3d.colors=c(...)}.  
#'        Otherwise, the default colors are \code{c("pink", "blue",
#'        "black", "darkgreen", "darkcyan", "magenta", "brown", "darkgray")}.
#' @param lwd a two-element vector giving the line width for drawing ellipsoids
#'        (including those that degenerate to an ellipse) and for drawing ellipsoids
#'        that degenerate to a line segment. The default is \code{c(1, 4)}.
#' @param shade a logical scalar or vector, indicating whether the ellipsoids
#'        should be rendered with \code{\link[rgl]{shade3d}}. Works like \code{col},
#'        except that \code{FALSE} is used for any 1 df degenerate ellipsoid.
#' @param shade.alpha a numeric value in the range [0,1], or a vector of such
#'        values, giving the alpha transparency for ellipsoids rendered with
#'        \code{shade=TRUE}.
#' @param wire a logical scalar or vector, indicating whether the ellipsoids
#'        should be rendered with \code{\link[rgl]{wire3d}}. Works like \code{col},
#'        except that \code{TRUE} is used for any 1 df degenerate ellipsoid.
#' @param bg.col background colour, \code{"white"} or \code{"black"},
#'        defaulting to \code{"white"}.
#' @param fogtype type of ``fog'' to use for depth-cueing; the default is
#'        \code{"none"}. See \code{\link[rgl]{bg}}.
#' @param fov field of view angle; controls perspective.  See
#'        \code{\link[rgl]{viewpoint}}.
#' @param offset proportion of axes to off set labels; defaults to \code{0.01}.
#' @param xlab x-axis label; defaults to name of the x variable.
#' @param ylab y-axis label; defaults to name of the y variable.
#' @param zlab z-axis label; defaults to name of the z variable.
#' @param xlim x-axis limits; if absent, will be computed from the data.
#' @param ylim y-axis limits; if absent, will be computed from the data.
#' @param zlim z-axis limits; if absent, will be computed from the data.
#' @param cex.label text size for ellipse labels
#' @param add if \code{TRUE}, add to the current plot; the default is
#'        \code{FALSE}.  If \code{TRUE}, the error ellipsoid is neither plotted nor
#'        returned in the output object.
#' @param verbose if \code{TRUE}, print the MANOVA table and details of
#'        hypothesis tests; the default is \code{FALSE}.
#' @param warn.rank if \code{TRUE}, do not suppress warnings about the rank of
#'        the hypothesis matrix when the ellipsoid collapses to an ellipse or line;
#'        the default is \code{FALSE}.
#' @param \dots arguments passed from generic.
#' @return \code{heplot3d} invisibly returns a list containing the bounding
#' boxes of the error (E) ellipsoid and for each term or linear hypothesis
#' specified in the call.  Each of these is a 2 x 3 matrix with rownames "min"
#' and "max" and colnames corresponding to the variables plotted. An additional
#' component, \code{center}, contains the coordinates of the centroid in the
#' plot.
#' 
#' The function also leaves an object named \code{.frame} in the global
#' environment, containing the rgl object IDs for the axes, axis labels, and
#' bounding box; these are deleted and the axes, etc.  redrawn if the plot is
#' added to.
#' @seealso 
#'    \code{\link[car]{Anova}}, \code{\link[car]{linearHypothesis}}, for
#'    details on MANOVA tests and linear hypotheses
#' 
#'    \code{\link{heplot}}, \code{\link{pairs.mlm}}, for other plotting methods
#'    for \code{mlm} objects
#' 
#'    \code{\link[rgl]{rgl-package}}, for details about 3D plots with \code{rgl}
#' 
#'    \code{\link[candisc]{heplot3d.candisc}} for 3D HE plots in canonical space.
#' 
#' @references 
#' Friendly, M. (2006).  Data Ellipses, HE Plots and Reduced-Rank
#' Displays for Multivariate Linear Models: SAS Software and Examples
#' \emph{Journal of Statistical Software}, 17(6), 1-42.
#' \url{https://www.jstatsoft.org/v17/i06/}
#' 
#' Friendly, M. (2007).  HE plots for Multivariate General Linear Models.
#' \emph{Journal of Computational and Graphical Statistics}, 16(2) 421-444.
#' \url{http://datavis.ca/papers/jcgs-heplots.pdf}
#' @keywords hplot aplot dynamic multivariate
#' @examples
#' 
#' # Soils data, from carData package
#' data(Soils, package = "carData")
#' soils.mod <- lm(cbind(pH,N,Dens,P,Ca,Mg,K,Na,Conduc) ~ Block + Contour*Depth, data=Soils)
#' car::Anova(soils.mod)
#' 
#' heplot(soils.mod, variables=c("Ca", "Mg"))
#' pairs(soils.mod, terms="Depth", variables=c("pH", "N", "P", "Ca", "Mg"))
#' 
#' heplot3d(soils.mod, variables=c("Mg", "Ca", "Na"), wire=FALSE)
#' 
#' # Plastic data
#' plastic.mod <- lm(cbind(tear, gloss, opacity) ~ rate*additive, data=Plastic)
#' \dontrun{
#' heplot3d(plastic.mod, col=c("red", "blue", "brown", "green3"), wire=FALSE)
#' }
#' 
#' @export heplot3d
heplot3d <-
function(mod, ...) UseMethod("heplot3d")

# TODO:
#  - add aspect argument (for candisc)

#' @rdname heplot3d
#' @exportS3Method heplot3d mlm
heplot3d.mlm <-
		function ( 
				mod,           # an mlm object
				terms,         # vector of terms to plot H ellipses
				hypotheses,    # list of linear hypotheses for which to plot H ellipses
				term.labels=TRUE,  # TRUE, FALSE or a list of term labels of length(terms)
				hyp.labels=TRUE,   # as above for term.labels
				err.label="Error",
				variables=1:3,     # x,y variables for the plot [variable names or numbers]
				error.ellipsoid=!add,
				factor.means=!add,
				grand.mean=!add,
				remove.intercept=TRUE,
				type=c("II", "III", "2", "3"),
				idata=NULL,
				idesign=NULL,
				icontrasts=c("contr.sum", "contr.poly"),
				imatrix=NULL,
				iterm=NULL,
				manova,        # an optional Anova.mlm object
				size=c("evidence", "effect.size"),
				level=0.68,
				alpha=0.05,
				segments=40,          # line segments in each ellipse
#				col=palette()[-1],    # colors for E matrix, H matrices
				col=getOption("heplot3d.colors", c("red", "blue", "black", "darkgreen", "darkcyan",
				                                   "magenta", "brown","darkgray")),
				# colors for H matrices, E matrix
				lwd=c(1, 4),          # line width for drawing ellipsoids and 1d degenerate ellipsoids
				shade=TRUE,           # use shade3d to render ellipsoids?
				shade.alpha=0.2,      # alpha transparency for shaded3d
				wire=c(TRUE,FALSE),   # use wire3d to render ellipsoids?
				bg.col=c("white", "black"),  # background colour
				fogtype=c("none", "exp2", "linear", "exp"), # fog -- for depth cueing
				fov=30,   # field of view (for perspective)
				offset=0.01, # for ellipsoid labels
				xlab,
				ylab,
				zlab,
				xlim,
				ylim,
				zlim,
        cex.label = 1.5,       # text size for ellipse labels
				add=FALSE,             # add to existing plot?
				verbose=FALSE,
				warn.rank=FALSE,  
				...) {              
	
	ellipsoid <- function(center, 
	                      shape, 
	                      radius=1, 
	                      label="",
	                      cex.label,
	                      col, 
	                      df=Inf, 
	                      shade=TRUE, 
	                      alpha=0.1, 
	                      wire=TRUE){
		# adapted from the shapes3d demo in the rgl package and from the Rcmdr package
		# modified to return the bbox of the ellipsoid
		degvec <- seq(0, 2*pi, length=segments)
		ecoord2 <- function(p) c(cos(p[1])*sin(p[2]), sin(p[1])*sin(p[2]), cos(p[2]))
		# v <- t(apply(expand.grid(degvec,degvec), 1, ecoord2))  # modified to make smoother
		v <- t(apply(expand.grid(degvec,degvec/2), 1, ecoord2)) 
		if (!warn.rank){
			warn <- options(warn=-1)
			on.exit(options(warn))
		}
		Q <- chol(shape, pivot=TRUE)
		lwd <- if (df < 2) lwd[2] else lwd[1]
		order <- order(attr(Q, "pivot"))
		v <- center + radius * t(v %*% Q[, order])
		v <- rbind(v, rep(1,ncol(v))) 
		e <- expand.grid(1:(segments-1), 1:segments)
		i1 <- apply(e, 1, function(z) z[1] + segments*(z[2] - 1))
		i2 <- i1 + 1
		i3 <- (i1 + segments - 1) %% segments^2 + 1
		i4 <- (i2 + segments - 1) %% segments^2 + 1
		i <- rbind(i1, i2, i4, i3)
		x <- rgl::asEuclidean(t(v))
		ellips <- rgl::qmesh3d(v, i)
		# override settings for 1 df line
		if (df<2) {
			wire <- TRUE
			shade <- FALSE
		}
		back <- if (df < 3) "culled" else "filled"
		depth_mask <- if (alpha <.8) FALSE else TRUE
		if (verbose) cat(paste("df=", df, "col:", col, " shade:", shade, " alpha:", alpha, 
		                         " wire:", wire, "back:", back, "depth_mask:", depth_mask,
		                       sep=" "), "\n")
		if(shade) rgl::shade3d(ellips, 
		                       col=col, alpha=alpha, lit=TRUE, back=back, depth_mask=depth_mask)
		if(wire) rgl::wire3d(ellips, 
		                     col=col, size=lwd, lit=FALSE, back=back, depth_mask=depth_mask)
		bbox <- matrix(rgl::par3d("bbox"), nrow=2)
		ranges <- apply(bbox, 2, diff)
		if (!is.null(label) && label !="")
			rgl::texts3d(x[which.max(x[,2]),] + offset*ranges, adj=0, texts=label, color=col, lit=FALSE)
		rownames(bbox) <- c("min", "max")
		return(bbox)
	}
	
	
	if (!requireNamespace("rgl")) stop("rgl package is required.")    
	# avoid deprecated warnings from car
	#if (car2 <- packageDescription("car")[["Version"]] >= 2) linear.hypothesis <- linearHypothesis

	type <- match.arg(type)
	size <- match.arg(size)
	fogtype <- match.arg(fogtype)
	bg.col <- match.arg(bg.col)    
	data <- model.frame(mod)
	if (missing(manova)) {
		if (is.null(imatrix)) {
			manova <- car::Anova(mod, type=type, idata=idata, idesign=idesign, icontrasts=icontrasts)
		}
		else {
				manova <- car::Anova(mod, type=type, idata=idata, idesign=idesign, icontrasts=icontrasts, imatrix=imatrix)
		} 
	}   
#	if (verbose) print(manova)    
#	response.names <- rownames(manova$SSPE)
	if (is.null(idata) && is.null(imatrix)) {
		Y <- model.response(data) 
		SSPE <- manova$SSPE
	} 
	else {
		if (is.null(iterm)) stop("Must specify a within-S iterm for repeated measures designs" )
		### FIXME::car -- workaround for car::Anova.mlm bug: no names assigned to $P component
		if (is.null(names(manova$P))) names(manova$P) <- names(manova$SSPE)
		Y <- model.response(data) %*% manova$P[[iterm]]
		SSPE <- manova$SSPE[[iterm]]
	}   
	
	if (!is.null(rownames(SSPE))) {response.names <- rownames(SSPE)}
	else {response.names <- paste("V.", 1:nrow(SSPE), sep="")}
	
	if (!is.numeric(variables)) {
		vars <- variables
		variables <- match(vars, response.names)
		check <- is.na(variables)
		if (any(check)) stop(paste(vars[check], collapse=", "), 
					" not among response variables.") 
	}
	else {
		if (any (variables > length(response.names))) stop("There are only ", 
					length(response.names), " response variables.")
		vars <- response.names[variables]
	}
	if (length(variables) != 3) {
		extra <- if (length(variables) == 1) 'heplot1d()' else 
				if (length(variables) == 2) 'heplot()' else 'pairs()'
		stop(paste("You may only plot 3 response variables. Use", extra))
	}
	
	if (missing(terms) || (is.logical(terms) && terms)) {
		terms <- manova$terms
		if (!is.null(iterm)) {
			terms <- terms[grep(iterm, terms)]   ## only include those involving iterm
		}
		if (remove.intercept) terms <- terms[terms != "(Intercept)"]
	}
	n.terms <- if (!is.logical(terms)) length(terms) else 0 
	# note: if logical here, necessarily FALSE
	n.hyp <- if (missing(hypotheses)) 0 else length(hypotheses)
	n.ell <- n.terms + n.hyp
	if (n.ell == 0) stop("Nothing to plot.")
	
	E <- SSPE
	p <- nrow(E)
	E <- E[variables, variables]
	Y <- Y[,vars] 
	gmean <- if (missing(data))  c(0,0,0) 
			else colMeans(Y)
	if (missing(xlab)) xlab <- vars[1]
	if (missing(ylab)) ylab <- vars[2]
	if (missing(zlab)) zlab <- vars[3]
	dfe <- manova$error.df
	scale <- 1/dfe 
	E <- E * scale
	radius <- sqrt(3 * qf(level, 3, dfe))
	
	col   <- he.rep(col, n.ell); E.col<- col[length(col)]
	shade <- he.rep(shade, n.ell)
	shade.alpha <- he.rep(shade.alpha, n.ell)
	wire  <- he.rep(wire, n.ell)
	
if (!add){    
		rgl::open3d()
		rgl::view3d(fov=fov)
		rgl::bg3d(color=bg.col, fogtype=fogtype)    
	} 
	
	if (error.ellipsoid) {
		E.ellipsoid <- ellipsoid(gmean, E, radius, col=E.col, 
		                         label=err.label, cex.label = cex.label,
		                         df=dfe,
				                    shade=shade[[length(shade)]], 
				                    alpha=shade.alpha[[length(shade.alpha)]],
				                    wire=wire[[length(wire)]])
		colnames(E.ellipsoid) <- vars
	}       
	term.labels <- if (n.terms == 0) NULL
			else if (!is.logical(term.labels)) term.labels
			else if (term.labels) terms else rep("", n.terms)
	
	H.ellipsoid <- as.list(rep(0, n.ell))
	if (n.terms > 0) for (term in 1:n.terms){
			term.name <- terms[term] 
			H <- manova$SSP[[term.name]]
			H <- H[variables, variables]
			dfh <- manova$df[term.name]
			#          scale <- eval(parse(text=h.scale))
			factor <- if (size == "evidence") lambda.crit(alpha, p, dfh, dfe) else 1  
			H <- H * scale/factor
			if (verbose){
				cat(term.name, " H matrix (", dfh, " df):\n")
				print(H)
			}
			if((!shade[term]) & !wire[term]) 
				warning(paste("shate and wire are both FALSE for ", term), call.=FALSE)
			H.ellipsoid[[term]] <- ellipsoid(gmean, H, radius, col=col[term], 
			                                 label=term.labels[term], cex.label = cex.label,
					                      df=dfh, shade=shade[term], alpha=shade.alpha[term], 
					                      wire=wire[term])  
			colnames(H.ellipsoid[[term]]) <- vars
		}
	hyp.labels <- if (n.hyp == 0) NULL
			else if (!is.logical(hyp.labels)) hyp.labels
			else if (hyp.labels) names(hypotheses) else rep("", n.hyp)  
	if (n.hyp > 0) for (hyp in 1:n.hyp){
			lh <- car::linearHypothesis(mod, hypotheses[[hyp]])
			H <- lh$SSPH[variables, variables]
			dfh <- lh$df
			factor <- if (size == "evidence") lambda.crit(alpha, p, dfh, dfe) else 1  
			H <- H * scale/factor
			if (verbose){
				cat("\n\n Linear hypothesis: ", names(hypotheses)[[hyp]], "\n") 
				print(lh)
			}
			term <- n.terms + hyp
			H.ellipsoid[[term]] <- ellipsoid(gmean, H, radius, col=col[term], 
			                                 label=hyp.labels[hyp], cex.label = cex.label,
					                             df=dfh, shade=shade[term], alpha=shade.alpha[term], wire=wire[term])
		}         
	ranges <- apply(matrix(rgl::par3d("bbox"), nrow=2), 2, diff)

	#   if (grand.mean) ellipsoid(gmean, diag((ranges/40)^2), col="black", wire=FALSE, alpha=0.8) # centre dot    
	# better: use a centered 3D cross here
	if (grand.mean) cross3d(gmean, (ranges/25), col="black", lwd=2) # centre cross            
	
#browser()	
	## BUG fixed here:  should only label the means for factors included in terms
	if ((!is.logical(factor.means)) || factor.means){
		factors <- data[, sapply(data, is.factor), drop=FALSE]
		factor.names <- colnames(factors) 
		if (is.null(iterm)) factor.names <- factor.names[factor.names %in% terms]
		if (!is.logical(factor.means)){
			which <- match(factor.means, factor.names)
			check <- is.na(which)
			if (any(check)) stop(paste(factor.means[check], collapse=", "), 
						" not among factors.")
			factors <- factors[, which, drop=FALSE]
		}
		else factors <- factors[, factor.names, drop=FALSE]    
#        for (fac in factors){
#            means <- aggregate(Y, list(fac), mean)
		if (ncol(factors)) for (j in 1:ncol(factors)){
				means <- aggregate(Y, list(factors[,j]), mean)
				# color the points the same as the ellipse for the term
				loc <- match(factor.names[j], terms, nomatch=0)
				pcol <- if (loc>0) col[loc] else "black"
				for (m in 1:nrow(means)) {
					ellipsoid(unlist(means[m, 2:4]), diag((ranges/100))^2, col=pcol, 
					          wire=FALSE, alpha=0.8)
#            		points3d(unlist(means[m, 2:4]), size=3, color=pcol)
#					spheres3d(unlist(means[m, 2:4]), radius=diag((ranges/30))^2, color=pcol)
				}
				rgl::texts3d(means[,2:4] + matrix(offset*ranges, nrow(means), 3, byrow=TRUE), 
						texts=as.character(means[,1]), color=pcol, adj=0)
			}
	}
	
	# handle xlim, ylim, zlim
	## enforce that the specified limits are at least as large as the bbox
	if (!missing(xlim) | !missing(ylim) | !missing(zlim)) {
		bbox <- matrix(rgl::par3d("bbox"),3,2,byrow=TRUE)
		xlim <- if(missing(xlim)) bbox[1,] else c(min(xlim[1],bbox[1,1]), max(xlim[2],bbox[1,2]))
		ylim <- if(missing(ylim)) bbox[2,] else c(min(ylim[1],bbox[2,1]), max(ylim[2],bbox[2,2]))
		zlim <- if(missing(zlim)) bbox[3,] else c(min(zlim[1],bbox[3,1]), max(zlim[2],bbox[3,2]))
		rgl::decorate3d(xlim=xlim, ylim=ylim, zlim=zlim, 
		                box=FALSE, axes=FALSE, 
		                xlab=NULL, ylab=NULL, zlab=NULL, top=FALSE)
	}
	
  # TODO: allow cex for axis labels
	if (add) rgl::pop3d(id=savedvars$.frame)
	frame <- rgl::axis3d("x-", color="black")
	frame <- c(frame, rgl::mtext3d(xlab, "x-", color="black", line=1.5))
	frame <- c(frame, rgl::axis3d("y-", col="black"))
	frame <- c(frame, rgl::mtext3d(ylab, "y-", color="black", line=1.5))
	frame <- c(frame, rgl::axis3d("z-", col="black"))
	frame <- c(frame, rgl::mtext3d(zlab, "z-", color="black", line=1.5))
	frame <- c(frame, rgl::box3d(col="black"))
	assign(".frame", frame, envir=savedvars)
	#   savedvars$.frame <- frame

	rgl::aspect3d(x=1, y=1, z=1)
	
	names(H.ellipsoid) <- c(if (n.terms > 0) term.labels, if (n.hyp > 0) hyp.labels)
	result <- if(error.ellipsoid) list(H=H.ellipsoid, E=E.ellipsoid, center=gmean, radius=radius) 
			else list(H=H.ellipsoid, center=gmean, radius=radius)
	class(result) <- "heplot3d"
	invisible(result)
	
}

.frame <- NULL   # avoid warning

Try the heplots package in your browser

Any scripts or data that you put into this service are public.

heplots documentation built on Sept. 8, 2023, 5:32 p.m.