Mayou
Mayou

Reputation: 8848

Adjust spacing between text in horizontal legend

I have a plot with a horizontal legend:

 legend("bottomleft", inset = c(0, -0.3), bty = "n",
        x.intersp=0, xjust=0,yjust=0,
        legend=c("AAPL", "Information Technology",
                 "Technology Hardware and Equipment", "S&P 500"),
        col=c("black", "red", "blue3", "olivedrab3"),
        lwd=2, cex = 0.5, xpd = TRUE, ncol = 4)

The problem is that there is a huge spacing between the first item of the legend, "AAPL", and the second item "Information Technology".

I tried adjusting the spacing using txt.width(), but it didn't work at all. Or maybe I am not using this option as directed. This is how I have introduced the txt.width option inside legend():

txt.width = c(2,1,1)

I am not sure if it relevant to mention, but my x-axis is an axis of dates!

Is there an easy way of customizing the spaces between the text in the legend?

Thanks!

Upvotes: 17

Views: 58993

Answers (7)

Balthasar
Balthasar

Reputation: 380

Setting textwidth = NA in legend() generates a

proper column wise maximum value of strwidth(legend)).

(Quoted from the R manual for legend().)

plot(1, 1, xlab = "", ylab = "", xlim = c(0, 2), ylim = c(0, 2))

legend(
  "bottomleft",
  text.width = NA,
  inset = c(0, -0.2), bty = "n", x.intersp = 0.5,
  xjust = 0, yjust = 0,
  legend = c(
    "AAPL", "Information Technology",
    "Technology Hardware and Equipment", "S&P 500"
  ),
  col = c("black", "red", "blue3", "olivedrab3"),
  lwd = 3, cex = 0.75, xpd = TRUE, horiz = TRUE
)

enter image description here

Did the trick for me when dealing with a similar situation.

Upvotes: 0

user3664601
user3664601

Reputation: 43

Based on the solution of pangia and the help of strwidth, it is possible to calculate the x coordinates from the legend text:

### plot needs to be called first, so that strwidth can work
plot(x=as.Date(c("1/1/13","3/1/13","5/1/13"), "%m/%d/%y"), y=1:3, ylim=c(0,3))

legtext <- c("AAPL", "Information Technology", 
    "Technology Hardware and Equipment", "S&P 500")

### the following lines calculate the xcoords
### note that the cex argument in strwidth should be
### the same as in the legend call
xcoords <- c(0, cumsum( strwidth(legtext, cex = 0.5))[-length(legtext)])
secondvector <- (1:length(legtext))-1
textwidths <- xcoords/secondvector 
textwidths[1] <- 0 

legend(x="bottomleft", bty = "n", x.intersp=0.5, xjust=0, yjust=0,
    legend=legtext, 
    col=c("black", "red", "blue3", "olivedrab3"), 
    lwd=2, cex = 0.5, xpd = TRUE, ncol = 4,
    text.width=textwidths)

Upvotes: 1

If anyone arrives at this same issue 7 years later, a technique I have found helpful is to set text.width with calls to the string width (strwidth) of each piece of legend text. This produces spacing which (to my eye) looks "natural" and can be modified by adding spaces to the strwidth argument. For the posed question, it would look something like this:

legend("bottomleft", inset = c(0, -0.3), bty = "n", x.intersp=0, xjust=0,yjust=0,
    legend=c("AAPL", "Information Technology",
             "Technology Hardware and Equipment", "S&P 500"),
    col=c("black", "red", "blue3", "olivedrab3"),
    lwd=2, cex = 0.5, xpd = TRUE, ncol = 4, text.width =
        c(strwidth("AAPL"), strwidth("Information Technology"), strwidth("Technology Hardware and Equipment"), strwidth("S&P 500"))
    )

This essentially does what Andre has suggested, but is a bit more user-friendly than manually defining the text width parameters by number.

Upvotes: 1

Hafiz Muhammad Shafiq
Hafiz Muhammad Shafiq

Reputation: 8680

For my case, there were 5 legends in horizontal way. I have to customize the spacing between each legend. Following was the code snippet for this purpose.

legend("topright",horiz = T, legend = df2, fill = col_box,
       inset = c(-0.2,-0.1), xpd = TRUE, bty = 'n', density = density_value, angle = angle_value, x.intersp=0.3,
       text.width=c(3.5,3.4,3.7,4.3,7))

It was the text.width function that do the magic

Upvotes: 2

RFelber
RFelber

Reputation: 164

On my system (platform: x86_64-w64-mingw32, R version: 3.4.1 (2017-06-30)) the solutions provided so far by Andre Silva and pangja are not satisfactory. Both solutions require user input and are dependent on the device size. Since I get never used to the text.width command and always had to adjust the values with try-and-error, I wrote the function (f.horlegend). The function has similar arguments as the function legend and is based on the idea posted here.

The function creates a horizontal (one row) legend, which can be positioned by the commands known from the function legend, e.g. "bottomleft"

f.horlegend <- function(pos, legend, xoff = 0, yoff = 0, 
  lty = 0, lwd = 1, ln.col = 1, seg.len = 0.04, 
  pch = NA, pt.col = 1, pt.bg = NA, pt.cex = par("cex"), pt.lwd = lwd, 
  text.cex = par("cex"), text.col = par("col"), text.font = NULL, text.vfont = NULL, 
  bty = "o", bbord = "black", bbg = par("bg"), blty = par("lty"), blwd = par("lwd"), bdens = NULL, bbx.adj = 0, bby.adj = 0.75 
) {

  ### get original par values and re-set them at end of function
  op <- par(no.readonly = TRUE)
  on.exit(par(op))

  ### new par with dimension [0,1]
  par(new=TRUE, xaxs="i", yaxs="i", xpd=TRUE)
  plot.new()

  ### spacing between legend elements
  d0 <- 0.01 * (1 + bbx.adj)
  d1 <- 0.01
  d2 <- 0.02
  pch.len <- 0.008
  ln.len <- seg.len/2

  n.lgd <- length(legend)

  txt.h <- strheight(legend[1], cex = text.cex, font = text.font, vfont = text.vfont) *(1 + bby.adj)
  i.pch <- seq(1, 2*n.lgd, 2)
  i.txt <- seq(2, 2*n.lgd, 2)

  ### determine x positions of legend elements
  X <- c(d0 + pch.len, pch.len + d1, rep(strwidth(legend[-n.lgd])+d2+pch.len, each=2))
  X[i.txt[-1]] <- pch.len+d1

  ### adjust symbol space if line is drawn
  if (any(lty != 0)) {
    lty <- rep(lty, n.lgd)[1:n.lgd]
    ln.sep <- rep(ln.len - pch.len, n.lgd)[lty]
    ln.sep[is.na(ln.sep)] <- 0
    X <- X + rep(ln.sep, each=2)
    lty[is.na(lty)] <- 0
  } 

  X <- cumsum(X)

  ### legend box coordinates
  bstart <- 0
  bend <- X[2*n.lgd]+strwidth(legend[n.lgd])+d0

  ### legend position
  if (pos == "top" | pos == "bottom" | pos == "center") x_corr <- 0.5 - bend/2 +xoff
  if (pos == "bottomright" | pos == "right" | pos == "topright") x_corr <- 1. - bend + xoff
  if (pos == "bottomleft" | pos == "left" | pos == "topleft") x_corr <- 0 + xoff

  if (pos == "bottomleft" | pos == "bottom" | pos == "bottomright") Y <- txt.h/2 + yoff
  if (pos == "left" | pos == "center" | pos =="right") Y <- 0.5 + yoff
  if (pos == "topleft" | pos == "top" | pos == "topright") Y <- 1  - txt.h/2 + yoff

  Y <- rep(Y, n.lgd)
  ### draw legend box
  if (bty != "n") rect(bstart+x_corr, Y-txt.h/2, x_corr+bend, Y+txt.h/2, border=bbord, col=bbg, lty=blty, lwd=blwd, density=bdens)

  ### draw legend symbols and text
  segments(X[i.pch]+x_corr-ln.len, Y, X[i.pch]+x_corr+ln.len, Y, col = ln.col, lty = lty, lwd = lwd)
  points(X[i.pch]+x_corr, Y, pch = pch, col = pt.col, bg = pt.bg, cex = pt.cex, lwd = pt.lwd)
  text(X[i.txt]+x_corr, Y, legend, pos=4, offset=0, cex = text.cex, col = text.col, font = text.font, vfont = text.vfont)

}

Arguments

pos position of the legend (c("bottomleft", "bottom", "bottomright", "left", "center", "right", "topleft", "top", "topright"))

legend legend text

xoff adjustement of position in x-direction. NB: the legend is plotted on a plot with limits = c(0,1)

yoff same as xoff but in y-directin

lty The line type. Line types can only be specified as an integer (0=blank, 1=solid (default), 2=dashed, 3=dotted, 4=dotdash, 5=longdash, 6=twodash)

lwd The line width, a positive number, defaulting to 1

ln.col The line color

seg.len The length of the line, deafult to 0.04

pch An integer specifying the symbol.

pt.col The symbol color.

pt.bg The background color of the symbol.

pt.cex expansion factor for the symbol

pt.lwd line width of symbol

text.cex expansion factor for the text

text.col text color

text.font text font

text.vfont see vfont in text-help

bty the type of box to be drawn around the legend. The allowed values are "o" (the default) and "n"

bbord color of the legend box border

bbg background color

blty border style

blwd border line width

bdens density of lines, see segment-help

bbx.adj relative value to increase space between text and horizontal box line

bby.adj same as bbx.adj but for vertical boc line

Unfortunately, I don't have time to create a package at the moment. But feel free to use the function. Any comments and ideas to improve the functions are welcome.

Some examples

plot(1:100, rnorm(100))
lgd.text <- c("12", "12")
sapply(c("bottomleft", "bottom", "bottomright", "left", "center", "right", "topleft", "top", "topright"), function(x) f.horlegend(x, lgd.text, pch=16, lty=c(NA, 1), bbg="orange"))


plot(1:100, rnorm(100))
lgd.text <- c("12", "132", "12345")
f.horlegend("topleft", lgd.text, pch=NA)
f.horlegend("top", lgd.text, pch=NA, bby.adj=1.5, bbord="red")
f.horlegend("left", lgd.text, xoff=0.2, pch=1, bby.adj=0, bbord="red", bbg="lightgreen")
f.horlegend("left", lgd.text, xoff=0.2, yoff=-0.05, pch=c(NA, 1, 6), lty=1, bbx.adj=2, bby.adj=0, bbord="red", bbg="lightgreen")

f.horlegend("topright", lgd.text, yoff=0.1, pch=c(1,2,NA), lty=c(NA, NA, 2), bbord="red", blty=2, blwd=2)

lgd.text <- c("12", "123456", "12345", "123")
f.horlegend("bottom", lgd.text, yoff=0.1, pch=c(1,2,NA), lty=c(NA, 1, 2), text.font=2)
f.horlegend("bottom", lgd.text, pch=c(1,2,NA), lty=c(NA, 1, 2), text.font=c(1,2,3))

plot(seq(as.POSIXct("2017-08-30"), as.POSIXct("2017-09-30"), by="weeks"), rnorm(5), type="l")
f.horlegend("topleft", "random values", lty=1)

Upvotes: 5

Andre Silva
Andre Silva

Reputation: 4928

plot(1,1,xlab="",ylab="",xlim=c(0,2),ylim=c(0,2))

legend("bottomleft", text.width=c(0,0.085,0.235,0.35),
       inset = c(0, -0.2), bty = "n", x.intersp=0.5,
       xjust=0, yjust=0,
       legend=c("AAPL", "Information Technology",
                "Technology Hardware and Equipment", "S&P 500"),
       col=c("black", "red", "blue3", "olivedrab3"),
       lwd=3, cex = 0.75, xpd = TRUE, horiz=TRUE)

horizontal_legend

I used text.width with four arguments to set the space between strings in the legend. The second argument inside text.width managed to set the distance between "AAPL" and "Information technology", and so on for the third and fourth arguments.

Unfortunately, I needed to reset the values inside text.width every time I changed the plot size.

Upvotes: 16

pangia
pangia

Reputation: 1164

text.width can give you control over the width of each column in your legend, but it's not straightforward. Basically, text.width is a vector that will be multiplied by another vector that is as long as your vector of legend strings. The elements of that second vector are integers from 0 to length(legend)-1. See the code to legend() for the gory details. The important thing is that you can think of this product of text.width and the second vector as, approximately, the x coordinates for your legend elements. Then if you know which x coordinates you want, you can calculate what needs to be passed in the text.width argument.

legtext <- c("AAPL", "Information Technology", 
             "Technology Hardware and Equipment", "S&P 500")
xcoords <- c(0, 10, 30, 60)
secondvector <- (1:length(legtext))-1
textwidths <- xcoords/secondvector # this works for all but the first element
textwidths[1] <- 0 # so replace element 1 with a finite number (any will do)

And then your final code could look something like this (except that we don't know your original plotting data or parameters):

plot(x=as.Date(c("1/1/13","3/1/13","5/1/13"), "%m/%d/%y"), y=1:3, ylim=c(0,3))

legend(x="bottomleft", bty = "n", x.intersp=0, xjust=0, yjust=0,
   legend=legtext, 
   col=c("black", "red", "blue3", "olivedrab3"), 
   lwd=2, cex = 0.5, xpd = TRUE, ncol = 4,
   text.width=textwidths)

As Andre Silva mentioned, the values you'll want in xcoords and textwidths will depend on the current size of your plot, the range of values specified for your x axis, etc.

Also, secondvector above would look different if you had more than one element per column. For example, for two columns with two legend elements apiece, secondvector == c(0,0,1,1).

Upvotes: 16

Related Questions