I am trying to wrap text labels in a rectangle.
Here is a simple plot with labels:
x = mtcars$wt
y = mtcars$mpg
l = rownames(mtcars)
plot(x, y)
text(x, y, l, adj = .5) # i.e., the default
I can successfully use strwidth and strheight to accomplish the wrapping like so:
delx = strwidth(l, cex = par('cex'))
dely = strheight(l, cex = par('cex'))
rect(x - .5*delx, y - .5*dely, x + .5*delx, y + .5*dely)
Notice the "gaps" of whitespace on the left and right of each label. For now, this is fine, but it leads to issues when trying to account for adj in the plot:
adj = c(0, .5)
plot(x, y)
text(x, y, l, adj = adj)
rect(
x - adj[1L]*delx, y - adj[2L]*dely,
x + (1-adj[1L])*delx, y + (1-adj[2L])*dely
)
The box has adjusted directionally well, but it appears to be an accurate transformation, I need to account further for whatever is creating the "buffer" width.
What is that? I haven't seen anything in ?par or ?strwidth yet.
There is no internal width buffer. It's just that you need to calculate the strwidth in the context of the current device. When you resize the graphics device after your plot is drawn, the rectangles will resize but the text will not. This will cause the apparent space around your text vary with the window size.
As long as you are careful to specify your device dimensions before drawing your plot, and recalculate strwidth for text on that device, your code should produce snug boxes around the text. You can add a fixed margin without affecting centering too, and none of this requires information that's not already available.
Here's a function for illustration purposes, that allows complete control of the boxes around your text:
michael_plot <- function(width = 9, height = 6, adj = c(0.5, 0.5), margin = 0)
{
dev.new(width = width, height = height, unit = "in", noRStudioGD = TRUE)
plot(x, y)
delx <- strwidth(l, cex = par('cex'))
xmarg <- strwidth("M", cex = par('cex')) * margin
dely <- strheight(l, cex = par('cex'))
ymarg <- strheight("M", cex = par('cex')) * margin / 2
text(x, y, l, adj = adj)
rect(x - adj[1] * delx - xmarg,
y - adj[2] * dely - ymarg,
x + (1 - adj[1]) * delx + xmarg,
y + (1 - adj[2]) * dely + ymarg)
}
Starting with the default:
michael_plot()
The boxes fit perfectly; in fact, they are too close to the text, so we should add a margin to make them more legible:
michael_plot(margin = 1)
But importantly, we can move them safely with adj while keeping them centred:
michael_plot(margin = 1, adj = c(0, 0.5))
Related
I am trying to convert the point size in cex to units of width and height on the plot. I can work with the units in inches, or pixels, or xy coordinates. I found something similar for finding the size of characters, but I can't find anything for plotted points. If I can figure out the size of a point when cex = 1 using par(), then I can calculate the sizes of differently scaled points too.
For characters, get height and width with:
par("cin") inches
par("cra") pixels
par("cxy") xy coordinates
Are there similar options to get the sizes of points?
OR, how do you convert character height and width into point diameter?
(Note that this similar question How to determine symbol size in x and y units only answers for characters and not for points.)
Through trial and error, it looks like the point diameter (in xy coordinates) is the character height divided by 2*pi.
char.height <- par("cxy")[2]
point.diam <- char.height / (2 * pi)
point.cex <- 7 # try changing and it still works
point.diam.cex <- point.diam * point.cex
plot(x = 0, y = 1, type = "n")
points(x = 0, y = 1, cex = point.cex, pch = 21, bg = "grey")
segments(x0 = 0 - (point.diam.cex/2), x1 = 0 + (point.diam.cex/2), y0 = 1, y1 = 1, col = "red")
I was running the example here, and noticed that the horizontal arrow connecting the Total to the Ineligible boxGrobs doesn't always touch the left-edge of the the Ineligible boxGrob.
It seems to depend on the width of the viewing window in RStudio. This does not seem to be the case for the vertical arrow, which always seem to perfectly connect to the top of the correct boxGrob.
Is there a way to force the arrow to touch the side of the box and not go any further? I am trying to save the output to a pdf, and by default it seems to use a wider plotting window so all of my horizontal arrows don't align with the correct boxes.
Narrow plotting window:
Wide plotting window:
I have tried manually creating a viewport with a wider area, but that didn't change anything in the pdf:
Code:
library(grid)
library(Gmisc)
vp <- grid::viewport(x = 10, y = 10, clip = 'on', xscale = c(0, 10),
yscale = c(0, 10), default.units = 'inch')
grid::pushViewport(vp)
leftx <- .25
midx <- .5
rightx <- .75
width <- .4
gp <- gpar(fill = "lightgrey")
# add box/connectors to the plot
(total <- boxGrob("Total\n N = NNN",
x=midx, y=.9, box_gp = gp, width = width))
(rando <- boxGrob("Randomized\n N = NNN",
x=midx, y=.75, box_gp = gp, width = width))
connectGrob(total, rando, "v")
(inel <- boxGrob("Ineligible\n N = NNN",
x=rightx, y=.825, box_gp = gp, width = .25, height = .05))
connectGrob(total, inel, "-")
For the time being, this problem can be solved using absolute unit.
Example code:
(inel <- boxGrob("Ineligible\n N = NNN",
x=rightx, y=.825, box_gp = gp, width = unit(2, "inch"), height = .05))
I'd like to add text to the right outer margin of multiple plots that is parallel to the axis but oriented towards the center of the plot (the orientation of the words "red" and "blue" in the below plot:
par(mfcol=2:1)
curve(sin,-2*pi,2*pi,col=2)
limits <- par("usr")
text(limits[2]+.25, mean(limits[3:4]),
"red", srt=270, xpd=T)
curve(sin,-2*pi,2*pi,col=4)
text(limits[2]+.25, mean(limits[3:4]),
"blue", srt=270, xpd=T)
mtext("Color of line",side=4,outer=T)
If the mtext function used the srt parameter rather than las (which was apparently the case for S plus), this would be trivial and the above workaround using usr would be unnecessary. But I'd like to be able to orient text in the outer margin ("Color of line" above) this same way, which I appear to be unable to do even manually with text (using xpd=T still constrains the text to the most recent figure region rather than the device region).
Is there a way to do this that doesn't require using layout as in the answer of #mrflick here? This seems like it should be trivial but I don't see how it can be done.
To find the y coordinates of the center of the device, you can use grconvertY to convert from "normalized device coordinates" ("ndc"; ranges from 0 to 1) to user coordinates.
The x value is here simply adjusted with an appropriate factor (e.g. limits[2] * 1.2).
windows()
par(mfrow = c(2, 1), oma = c(0, 0, 0, 2))
curve(sin, -2*pi, 2*pi, col = 2)
limits <- par("usr")
text(limits[2] + 0.25, mean(limits[3:4]),
"red", srt = 270, xpd = TRUE)
curve(sin, -2*pi, 2*pi, col = 4)
text(limits[2] + 0.25, mean(limits[3:4]),
"blue", srt = 270, xpd = TRUE)
text(x = limits[2] * 1.2, y = grconvertY(0.5, from = "ndc"),
labels = "color of line", xpd = NA, srt = 270)
Please see previous revisions if you rather want to calculate y position from user coordinates ("usr") and plot margins ("mai").
I have a simple scatter plot, where I want to add
some text fields. Additionally, I want to put a frame around them.
Here is a toy example:
set.seed(1)
x <- rnorm(10)
y <- rnorm(10)
plot(x,y)
text(0,0,'FRAME ME PLEASE')
It's possible to do this dynamically if you calculate the width and height of the string in plotting units:
set.seed(1); x <- rnorm(10); y <- rnorm(10); plot(x,y)
txt <- 'FRAME ME PLEASE'
xt <- 0
yt <- 0
text(xt, yt, txt)
sw <- strwidth(txt)
sh <- strheight(txt)
frsz <- 0.05
rect(
xt - sw/2 - frsz,
yt - sh/2 - frsz,
xt + sw/2 + frsz,
yt + sh/2 + frsz
)
It is worth noting that this can also deal with cex and font changes in the width and height calculation stages if specified.
Here's another option making legend do the work.
legend(0, 0, "FRAME ME PLEASE",
xjust = 0.5, # 0.5 means center adjusted
yjust = 0.5, # 0.5 means center adjusted
x.intersp = -0.5, # adjust character interspacing as you like to effect box width
y.intersp = 0.1, # adjust character interspacing to effect box height
adj = c(0, 0.5)) # adjust string position (default values used here)
# cex = 1.5, # change cex if you like (not used here)
# text.font = 2) # bold the text if you like (not used here)
rect(-0.4,-0.1, 0.4,0.1, border=1) Should do the trick, but I just hacked around to find the position. If you are making graphs with dynamically generated text, you may have to work harder to position the rectangle.
I use the following script to generate a legend in R. But the legend box is too small... how do I increase the box width?
legend("topleft", lty = 1, legend = c("Sub_metering_1","Sub_metering_2","Sub_metering_3"),col = c("black","red","blue"))
You are probably resizing your graph after you plot it and the legend. If that is the case, and you want to keep the box, one option would be to plot the graph, resize it, and then generate the legend. Perhaps a better option would be to size the window to the desired width to start with:
# on Windows, you can use the `windows` function. elsewhere, try quartz or X11
windows(height = 7, width = 3.5)
plot(hp ~ mpg, data = mtcars)
leg <- legend("topleft", lty = 1,
legend = c("Sub_metering_1","Sub_metering_2","Sub_metering_3"),
col = c("black","red","blue"),
#plot = FALSE,
#bty = "n")
)
You can also define exactly where you want the box to fall by providing a pair of x and y coordinates to the legend function. Those values would represent the upper left and bottom right corners of the box. The legend function will actually generate the coordinates for the upper-left hand corner of the box along with the width and height. By default it returns them invisibly, but you can assign them to an object, and If you use the plot = FALSE, option to legend you can capture those coordinates and modify them as you wish without actually plotting the legend.
windows(height = 7, width = 3.5)
plot(hp ~ mpg, data = mtcars)
legend(x = c(9.46, 31), y = c(346.32, 298),
legend = c("Sub_metering_1","Sub_metering_2","Sub_metering_3"),
col = c("black","red","blue"),
lty = 1)
The legend function will actually generate the coordinates for the upper-left hand corner of the box (that's where I got 9.46 and 346.62) along with the width and height of the box. By default it returns them invisibly, but you can assign them to an object, and if you use the plot = FALSE, option to legend you can capture those coordinates and modify them as you wish without actually plotting the legend.
plot(hp ~ mpg, data = mtcars)
leg <- legend("topleft", lty = 1,
legend = c("Sub_metering_1","Sub_metering_2","Sub_metering_3"),
col = c("black","red","blue"),
plot = FALSE)
# adjust as desired
leftx <- leg$rect$left
rightx <- (leg$rect$left + leg$rect$w) * 1.2
topy <- leg$rect$top
bottomy <- (leg$rect$top - leg$rect$h) * 1
# use the new coordinates to define custom
legend(x = c(leftx, rightx), y = c(topy, bottomy), lty = 1,
legend = c("Sub_metering_1","Sub_metering_2","Sub_metering_3"),
col = c("black","red","blue"))
Part of the legend width is determined by the longest width of the labels you use, which is calculated via strwidth. Below an easy example how to halve or double the size by using legend(..., text.width = ...).
plot(1)
text = c("Sub_metering_1","Sub_metering_2","Sub_metering_3")
legend("topleft"
,lty = 1
,legend = text
,col = c("black","red","blue")
)
strwidth(text)
# [1] 0.1734099 0.1734099 0.1734099
# half the length
legend("bottomleft"
,lty = 1
,legend = text
,text.width = strwidth(text)[1]/2
,col = c("black","red","blue")
)
# double the length
legend("center"
,lty = 1
,legend = text
,text.width = strwidth(text)[1]*2
,col = c("black","red","blue")
)