I am creating a CDF plot function more user-friendly than any default r function. Depending upon the bimodality of the data (it is often bimodal), or any other strange data trends, the points can end up gathering in just about any corner of the plot. So, when I add a legend, whether I choose to add it in the bottom right, top left, or wherever, it will sometimes end up putting the legend right on the data itself. So, I tried to force the legend to certain data values (e.g., the largest in x & the smallest in y, to "really" get the bottom right), but then this means most of the legend is missing because it is outside of the "plot region". How can I create a legend that is fully outside of the plot, similar to what excel does by default, for example? Thanks! Mike [[alternative HTML version deleted]]
I also have this problem! I would be great to hear a solution. Mike Williamson-9 wrote:> > I am creating a CDF plot function more user-friendly than any default > r > function. Depending upon the bimodality of the data (it is often > bimodal), > or any other strange data trends, the points can end up gathering in just > about any corner of the plot. So, when I add a legend, whether I choose > to > add it in the bottom right, top left, or wherever, it will sometimes end > up > putting the legend right on the data itself. So, I tried to force the > legend to certain data values (e.g., the largest in x & the smallest in y, > to "really" get the bottom right), but then this means most of the legend > is > missing because it is outside of the "plot region". > How can I create a legend that is fully outside of the plot, similar > to > what excel does by default, for example? > > Thanks! > Mike > > [[alternative HTML version deleted]] > > ______________________________________________ > R-help at r-project.org mailing list > https://stat.ethz.ch/mailman/listinfo/r-help > PLEASE do read the posting guide > http://www.R-project.org/posting-guide.html > and provide commented, minimal, self-contained, reproducible code. > >-- View this message in context: http://www.nabble.com/How-to-get-legend-outside-of-plot--tp21448346p21449657.html Sent from the R help mailing list archive at Nabble.com.
Dear Mike: 1. Might it work to use legend(x = if(median(data) > mean(range(data))) "topleft", else "bottomright", ...)? 2. The first two hits to 'RSiteSearch("points outside plot region")' suggested setting "par(xpd=TRUE)", then specifying "x" and "y" in "legend" appropriately outside the plot region. For example, par(mar=c(5, 4, 4, 11)+0.1, xpd=TRUE) would leave room on the left hand side of the plot. Then legend(max(x), max(y), ...) would put the legend top right outside the plot. The following is a modification of the first example from the "legend" help page: leg.txt <- c("Setosa Petals", "Setosa Sepals", "Versicolor Petals", "Versicolor Sepals") op <- par(mar=c(5, 4, 4, 11)+.1, xpd=TRUE) legend(0, -1, leg.txt) matplot(c(1,8), c(0,4.5), type = "n", xlab = "Length", ylab = "Width", main = "Petal and Sepal Dimensions in Iris Blossoms") legend(9, 4.5, leg.txt, pch = "sSvV", col = c(1, 3)) par(op) 3. The "lattice" package offers more control for this kind of thing. This package is quite valuable for multivariate displays of all kinds. Unfortunately, I found it difficult to learn. For me an essential reference is Sarkar (2008) Lattice: Multivariate Data Visualization with R (Springer). Hope this helps. Un fuerte abrazo, Spencer Mike Williamson wrote:> I am creating a CDF plot function more user-friendly than any default r > function. Depending upon the bimodality of the data (it is often bimodal), > or any other strange data trends, the points can end up gathering in just > about any corner of the plot. So, when I add a legend, whether I choose to > add it in the bottom right, top left, or wherever, it will sometimes end up > putting the legend right on the data itself. So, I tried to force the > legend to certain data values (e.g., the largest in x & the smallest in y, > to "really" get the bottom right), but then this means most of the legend is > missing because it is outside of the "plot region". > How can I create a legend that is fully outside of the plot, similar to > what excel does by default, for example? > > Thanks! > Mike > > [[alternative HTML version deleted]] > > ______________________________________________ > R-help at r-project.org mailing list > https://stat.ethz.ch/mailman/listinfo/r-help > PLEASE do read the posting guide http://www.R-project.org/posting-guide.html > and provide commented, minimal, self-contained, reproducible code. > >
Hi, I think this is a very common question on this list. I've just created a page in the R wiki (inspired by https://stat.ethz.ch/pipermail/r-help/2007-May/132466.html) . With some suggestions and improvements, hopefully we can make a good reference for others to refer to in the future: http://wiki.r-project.org/rwiki/doku.php?id=tips:graphics-misc:legendoutside Hope this helps, baptiste On 14 Jan 2009, at 01:27, Mike Williamson wrote:> I am creating a CDF plot function more user-friendly than any > default r > function. Depending upon the bimodality of the data (it is often > bimodal), > or any other strange data trends, the points can end up gathering in > just > about any corner of the plot. So, when I add a legend, whether I > choose to > add it in the bottom right, top left, or wherever, it will sometimes > end up > putting the legend right on the data itself. So, I tried to force the > legend to certain data values (e.g., the largest in x & the smallest > in y, > to "really" get the bottom right), but then this means most of the > legend is > missing because it is outside of the "plot region". > How can I create a legend that is fully outside of the plot, > similar to > what excel does by default, for example? > > Thanks! > Mike > > [[alternative HTML version deleted]] > > ______________________________________________ > R-help at r-project.org mailing list > https://stat.ethz.ch/mailman/listinfo/r-help > PLEASE do read the posting guide http://www.R-project.org/posting-guide.html > and provide commented, minimal, self-contained, reproducible code._____________________________ Baptiste Augui? School of Physics University of Exeter Stocker Road, Exeter, Devon, EX4 4QL, UK Phone: +44 1392 264187 http://newton.ex.ac.uk/research/emag
Bengoechea Bartolomé Enrique (SIES 73)
2009-Jan-14 14:53 UTC
[R] How to get legend outside of plot?
> How can I create a legend that is fully outside of the plot, similar to > what excel does by default, for example?The common solution with traditional plots (pre-modifying the margin) works well for a one-shot plot, where you calculate the legend size by trial-and-error. The problem arises when you need to automate this process for different legend texts/sizes. The best solution in this case is to use lattice or ggplot, as others have pointed. If still you want to stick to traditional plots, I have developed the functions below that may help. The idea is to estimate the legend size in an absolute measure unit like inches prior to plotting. This code is preliminary and not very well tested, so you may need to modify it. The legend size estimation algorithm is very simple, it can be improved by matching what the legend() function really works, but it works well with most of my plots. A basic drawback is that the legend is not redrawn correctly when the plot window is resized.... Run this example client code to see how it works: x <- 0:64/64; legendText <- paste("sin(", 1:5, "pi * x)"); oldPar <- par(ask=FALSE, mai=c(1.02, 0.82, 0.82, 0.42)); for (location in c("outright", "outbottom", "outleft", "outtop")) { if (!identical(location, "outright")) par(ask=TRUE, mai=c(1.02, 0.82, 0.82, 0.42)); estimate.legend.size(location, legendText, col=1:5, lty=1:5, pch="*", cex=0.8); matplot(x, outer(x, 1:5, function(x, k) sin(k * pi * x)), type="o", col=1:5, ylim= c(-1, 1.5), pch="*", main="TITLE"); legend(0, 1.5, legendText, col=1:5, lty=1:5, pch="*", ncol=3, cex=0.8); place.legend(location, legendText, col=1:5, lty=1:5, pch="*", cex=0.8); } par(oldPar); Suggestions for improvement welcomed! Best, Enrique # ---------------------------------------------------------------------------------------- #' Converts distances between margin units. Possible units are inches, columns of text, or user coordinates. #' #' @param width numeric with the distance width in the \code{input} units. #' @param height numeric with the distance height in the \code{input} units. #' @param input string with the input units. The valid values are \code{"inches"}, \code{"mlines"} (margin lines), #' and \code{"user"} (user plot coordinates). #' #' @returns A list with elements \code{"inches"}, \code{"mlines"}, and \code{"user"}, each of which is a list with #' elements \code{width} and \code{height}. #' #' @seealso \link{\codegrconvertX}}, \link{\codegrconvertY}}. cnvrt.plot.distance <- function(width=NA, height=NA, input=c("inches", "mlines", "user")) { n <- max(length(width), length(height)); width <- rep(as.numeric(width), length.out=n); height <- rep(as.numeric(height), length.out=n); input <- match.arg(input); cusr <- par('usr'); # Extremes c(x1, x2, y1, y2) of the user coordinates of the plotting region. cpin <- par('pin'); # The current plot dimensions (width, height), in inches. ccin <- par('cin'); # Character size (width, height) in inches. cmex <- par('mex'); # A character size expansion factor which is used to describe coordinates in the # margins of plots. Note that this does not change the font size, rather specifies # the size of font (as a multiple of csi) used to convert between mar and mai, and between oma and omi. if (input == "inches") { inches <- list(width = width , height = height); user <- list(width = width / cpin[1] * (cusr[2] - cusr[1]), height = height / cpin[2] * (cusr[4] - cusr[3])); mlines <- list(width = width / ccin[2] / cmex, height = height / ccin[2] / cmex ); } else if (input == "mlines") { mlines <- list(width = width , height = height); inches <- list(width = width * ccin[2] * cmex, height = height * ccin[2] * cmex ); user <- list(width = inches$width / cpin[1] * (cusr[2] - cusr[1]), height = inches$height / cpin[2] * (cusr[4] - cusr[3])); } else if (input == "user") { user <- list(width = width , height = height); inches <- list(width = width * cpin[1] / (cusr[2] - cusr[1]), height = height * cpin[2] / (cusr[4] - cusr[3])); mlines <- list(width = inches$width / ccin[2] / cmex, height = inches$height / ccin[2] / cmex ); } list(inches=inches, mlines=mlines, user=user); } # ---------------------------------------------------------------------------------------- #' Modifies plot margins. #' #' @param side a number in 1:4 or a string specifying the margin side to modify. The allowed values for strings #' are \code{"bottom"} (the default), \code{"left"}, \code{"top"}, and \code{"right"}. #' @param by the amount to add/substract to the current margin. It can be one of the following three types: #' 1) a number; 2) a list at least two numeric items named \code{width} and \code{height} (only one of them will be #' used, depending on the margin side being set); or 3) a character vector, whose width as computed by #' \link{\code{strwidth}} will be \code{by}. #' @param input string defining the unit of measure of the input (when \code{by} is numeric of a list with numeric elements). #' The allowed values are \code{"inches"} (the default), \code{"mlines"}, and \code{"user"}. #' @param which string defining which plot margin should be set. The valid values are \code{"mai"} (the default), \code{"omi"}, #' and \code{"mar"}. #' @param current numeric vector of length 4 that defines the current value of the margins that should be modified. #' Defaults to the corresponding graphical parameter \code{par(which)}. #' @param set.par logical. Whether the corresponding graphical parameter should be automatically modified using \code{par}. #' @param ... additional arguments to be passed to function \code{strwidth} when \code{by} is a character vector. #' #' @returns A numeric vector of length 4 with the new margin values. Note that when \code{set.par} is \code{TRUE}, also #' modifies \code{par(which)} as a side effect. modify.margin <- function(side=c("bottom", "left", "top", "right"), by, input=c("inches", "mlines", "user"), which=c("mai", "omi", "mar"), current=par(which), set.par=FALSE, ...) { defaultSides <- c("bottom", "left", "top", "right"); if (is.numeric(side)) { nSide <- side; side <- defaultSides[nSide]; } else { side <- match.arg(side, defaultSides); nSide <- match(side, defaultSides); } input <- match.arg(input); which <- match.arg(which); # Compute size if 'by' is a character vector targetUnit <- c("inches", "inches", "mlines")[match(which, c("mai", "omi", "mar"))]; isVertical <- (side %in% c("bottom", "top")); if (is.character(by)) { by <- max(strwidth(by, units="inches", ...), na.rm=TRUE); input <- "inches"; } else if (is.list(by) && all(c("width", "height") %in% names(by))) { by <- if (isVertical) by$height else by$width; } else if (!is.numeric(by) && length(by) == 1) stop(gettextf("'%s' must be either a number, a list with 'width' and 'height' components, or a character vector", "by")); # Convert input distance to target unit depending on which margin we're setting. if (input != targetUnit) { by <- if (isVertical) { cnvrt.plot.distance(width=by, input=input)[[targetUnit]]$width; } else cnvrt.plot.distance(height=by, input=input)[[targetUnit]]$height; } # Modify the margin value result <- current; result[nSide] <- result[nSide] + by; # Set the plot parameter if (set.par) { parValue <- list(result); names(parValue) <- which; do.call("par", parValue); } result; } # ---------------------------------------------------------------------------------------- #' A wrapper for \link{\code{legend}} that provides new location options for positioning the legend outside the plot area. #' #' @param location where the legend will be located. This can be a numeric vector of length 2 defining the \code{x} and #' \code{y} user coordinates, or a string with allowed values \code{"outtop"}, \code{"outbottom"}, \code{"outleft"}, #' \code{"outright"}, \code{"bottomright"}, \code{"bottom"}, \code{"bottomleft"}, \code{"left"}, \code{"topleft"}, #' \code{"top"}, \code{"topright"}, \code{"right"}, and \code{"center"}. #' @param ... further parameters passed to the \link{\code{legend}} function. #' #' @returns The same list as \code{legend} (invisibly). #' #' @details For positioning outside the plot area, client code should ensure that enough space is available on the corresponding #' margin, which can be done using \code{estimate.legend.size(..., adjust.margin="mai")} before the actual plotting is done. #' #' Note that some aesthetic defaults have been changed to adapt to corporate guidelines, e.g. not drawing the box around the legend, #' or outtop/outbottom legend positioning close to the edge. #' #' @seealso \link{\code{legend}}, \link{\code{estimate.legend.size}}. #' @notes ToDo: Add also positioning options for smart inside, and interactive. #' For smart inside positioning see \code{plotrix::emptyspace(x.index, x, bar=TRUE)}. place.legend <- function(location, ...) { dots <- list(...); if (is.null(dots$bty)) dots$bty <- "n"; if (is.numeric(location)) if (length(location) == 2) return(do.call("legend", c(list(x=location[1], y=location[2]), dots))) else stop(gettextf("Format of '%s' not recognized", "location")); location <- match.arg(location, c("outtop", "outbottom", "outleft", "outright", "bottomright", "bottom", "bottomleft", "left", "topleft", "top", "topright", "right", "center")); if (substr(location, 1, 3) == "out") { if (is.null(dots$horiz)) dots$horiz <- (location %in% c("outtop", "outbottom")); dots$xjust <- switch(location, outleft=0, outright=1, outtop=, outbottom=0.5); dots$yjust <- switch(location, outleft=, outright=0.5, outtop=1, outbottom=0); dots$x <- switch(location, outtop = , outbottom = grconvertX(0.5, from="ndc", to="user"), outleft = grconvertX(0, from="ndc", to="user"), outright = grconvertX(1, from="ndc", to="user") ) dots$y <- switch(location, outleft = , outright = grconvertY(0.5, from="ndc", to="user"), outtop = grconvertY(1, from="ndc", to="user"), outbottom = grconvertY(0, from="ndc", to="user") ) oxpd <- par(xpd=NA); on.exit(par(oxpd)); do.call("legend", dots); } else do.call("legend", c(list(x=location, y=NULL), dots)); } # ---------------------------------------------------------------------------------------- #' Legend Size #' #' Estimates the plot space required for the specified legend, in inches. #' #' @param location a string defining where the legend will be located #' @param legend a character or expression vector of length >= 1 to appear in the legend. See \link{legend}. #' @param cex character expansion factor \bold{relative} to current \code{par("cex")}. See \link{legend}. #' @param x.intersp character interspacing factor for horizontal (x) spacing. See \link{legend}. #' @param y.intersp character interspacing factor for vertical (y) spacing. See \link{legend}. #' @param ncol the number of columns in which to set the legend items (default is 1, a vertical legend). See \link{legend}. #' @param horiz logical; if \code{TRUE}, set the legend horizontally rather than vertically. Specifying \code{horiz} overrides #' the \code{ncol} specification. If \code{location} is defined, \code{horiz} defaults to \code{TRUE} for legends on the #' left or right margins, and to \code{FALSE} for legends on the top or bottom margins. #' @param title a character string or length-one expression giving a title to be placed at the top of the legend. See \link{legend}. #' @param direct logical; If \code{TRUE}, the estimation is done directly using the \code{legend} function with \code{plot=FALSE} #' argument, otherwise using an approximation. \code{TRUE} produces an exact measure but can only be computed #' if the graphics device is open and user coordinates have been set. Otherwise, use indirect estimation. #' @param adjust.margin a string defining which margin should be automatically resized, if any. Use \code{NULL} for no #' adjustment. The allowed values are \code{"mar"}, \code{"mai"}, and \code{"omi"}. #' #' @return A list with components \code{width} and \code{height} giving the legend estimated size in inches. #' #' @details This is to be used by traditional plots functions with legends outside the plot area to leave enough space in the #' corresponding margin before plotting the data. #' #' ToDo: Not all possible legend cases have been handled in indirect estimation, just some basic ones. This needs #' further development and testing. #' #' @seealso \link{\code{place.legend}}, \link{\code{legend}}. estimate.legend.size <- function(location=c("outright", "outleft", "outtop", "outbottom"), legend=NULL, cex=1, x.intersp=1, y.intersp=1, ncol=1, horiz=(location %in% c("outtop", "outbottom")), title=NULL, direct=FALSE, adjust.margin=c("mai", "omi", "mar"), ...) { location <- match.arg(location); result <- list(width=0, height=0); if (is.null(legend)) return(result) else legend <- as.character(legend); if (direct) { # Note: Can this really be done? If then we use this measures to modify 'mai', won't the new user coordinates change # their mapping to inches? legendSize <- legend(x=par("usr")[1], y=par("usr")[4], legend=legend, cex=cex, x.intersp=x.intersp, y.intersp=y.intersp, ncol=ncol, horiz=horiz, title=title, plot=FALSE, ...); result <- cnvrt.plot.distance(legendSize$rect$w, legendSize$rect$h, input="user")$inches; } else { labelWidth <- max(strwidth(legend, units="inches", cex=cex), na.rm=TRUE); labelHeight <- max(strheight(legend, units="inches", cex=cex), na.rm=TRUE); interspWidth <- strwidth(paste(rep(" ", times=x.intersp), collapse=""), units="inches", cex=cex); interspHeight <- strheight(paste(rep(" ", times=y.intersp), collapse=""), units="inches", cex=cex); symbolWidth <- strwidth(" ", units="inches", cex=cex); titleHeight <- if (is.null(title)) 0 else strheight(title, units="inches", cex=cex); if (horiz) { if (ncol == 1) { ncol <- length(legend); nrow <- 1; } else nrow <- ceiling(length(legend) / ncol); } else { if (ncol == 1) { nrow <- length(legend); } else nrow <- ceiling(length(legend) / ncol); } result$width <- ncol * (labelWidth + symbolWidth) + (ncol - 1) * interspWidth; result$height <- nrow * labelHeight + (nrow - 1) * interspHeight + titleHeight; } if (!is.null(adjust.margin)) modify.margin(match(location, c("outbottom", "outleft", "outtop", "outright")), by=result, input="inches", which=adjust.margin, set.par=TRUE); result; }