Put a frame around text fields in R base plots - r

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.

Related

How do I add multiple subplots into a multirow figure in R?

i need to overlay multiple subplots onto a single plot which is already contained inside a multirow figure (see image)
the reason why i need subplots instead of screen layout is because the figure will be possibly multicolumn, also (a 5 by 3 plot, in total)
there are packages which assist in doing subplots, but they break when you use multirow figures, and sequential subplots, except the first one, are rendered next to the overall figure border, not relative to the current row/column plot borders
i understand large packages such as ggplot2 allow this relatively easily, but base R plots are highly preferable
UPD:
the minimum reproducible example depicting the problem is here:
require(Hmisc)
COL.1 <- c('red','orange','yellow'); COL.2 <- c('blue','green','turquoise')
SUBPLOT.FUN <- function(COL) {plot(rnorm(100), type='l', col=COL)}
PLOT.FUN <- function(i) {
plot(rnorm(100),ylim=c(-1,1))
subplot(SUBPLOT.FUN(COL.1[i]), 100,1, vadj=1,hadj=1,pars=list(mfg=c(1,i)))
subplot(SUBPLOT.FUN(COL.2[i]), 100,-1,vadj=0,hadj=1,pars=list(mfg=c(1,i)))
}
plot.new(); par(mfrow=c(1,3))
for (i in 1:3) {
PLOT.FUN(i)
}
which looks like that:
while what is required is shown on the first image (meaning, EACH of the three plots must contain 3 subplots in their respective locations (along the right border, arranged vertically))
N.B. either the figure is multirow or multicolumn (as depicted) does not matter
Something like this? Inspired in this R-bloggers post.
# reproducible test data
set.seed(2022)
x <- rnorm(1000)
y <- rbinom(1000, 1, 0.5)
z <- rbinom(1000, 4, 0.5)
# save default values and prepare
# to create custom plot areas
old_par <- par(fig = c(0,1,0,1))
# set x axis limits based on data
h <- hist(x, plot = FALSE)
xlim <- c(h$breaks[1] - 0.5, h$breaks[length(h$breaks)] + 2)
hist(x, xlim = xlim)
# x = c(0.6, 1) right part of plot
# y = c(0.5, 1) top part of plot
par(fig = c(0.6, 1, 0.5, 1), new = TRUE)
boxplot(x ~ y)
# x = c(0.6, 1) right part of plot
# y = c(0.1, 0.6) bottom part of plot
par(fig = c(0.6, 1, 0.1, 0.6), new = TRUE)
boxplot(x ~ z)
# put default values back
par(old_par)
Created on 2022-08-18 by the reprex package (v2.0.1)

Left-aligning ggplot when saved while using a fixed aspect ratio

I'm building a custom ggplot theme to standardize the look & feel of graphs I produce. The goal is more complex than this minimal example, so I'm looking for a general solution. I have a few key goals:
I want all graphs to export at the same size (3000 pixels wide, 1500 pixels high).
I want to control the aspect ratio of the plot panel itself.
I want to use textGrobs to include figure numbers.
I want the image to be left-aligned
The challenge I'm facing is that when combining these two constraints, the image that gets saved centers the ggplot graph within the window, which makes sense as a default, but looks bad in this case.
I'm hoping there's a general solution to left-align the ggplot panel when I export. Ideally, this will also work similarly for faceted graphs.
It seems that something should be possible using one of or some combination of the gridExtra, gtable, cowplot, and egg packages, but after experimenting for a few hours I'm at a bit of a loss. Does anybody know how I can accomplish this? My code is included below.
This is the image that gets produced. As you can see, the caption is left-aligned at the bottom, but the ggplot itself is horizontally centered. I want the ggplot graph left-aligned as well.
Graph output: https://i.stack.imgur.com/5EM2c.png
library(ggplot2)
# Generate dummy data
x <- paste0("var", seq(1,10))
y <- LETTERS[1:10]
data <- expand.grid(X=x, Y=y)
data$Z <- runif(100, -2, 2)
# Generate heatmap with fixed aspect ratio
p1 <- ggplot(data, aes(X, Y, fill= Z)) +
geom_tile() +
labs(title = 'A Heatmap Graph') +
theme(aspect.ratio = 1)
# A text grob for the footer
figure_number_grob <- grid::textGrob('Figure 10',
x = 0.004,
hjust = 0,
gp = grid::gpar(fontsize = 10,
col = '#01A184'))
plot_grid <- ggpubr::ggarrange(p1,
figure_number_grob,
ncol = 1,
nrow = 2,
heights = c(1,
0.05))
# save it
png(filename = '~/test.png', width = 3000, height = 1500, res = 300, type = 'cairo')
print(plot_grid)
dev.off()
I was able to find a solution to this that works for my needs, though it does feel a bit hacky.
Here's the core idea:
Generate the plot without a fixed aspect ratio.
Split the legend from the plot as its own component
Use GridExtra's arrangeGrob to combine the plot, a spacer, the legend, and another spacer horizontally
Set the width of the plot to some fraction of npc (normal parent coordinates), in this case 0.5. This means that the plot will take up 50% of the horizontal space of the output file.
Note that this is not exactly the same as setting a fixed aspect ratio for the plot. If you know the size of the output file, it's close to the same thing, but the size of axis text & axis titles will affect the output aspect ratio for the panel itself, so while it gets you close, it's not ideal if you need a truly fixed aspect ratio
Set the width of the spacers to the remaining portion of the npc (in this case, 0.5 again), minus the width of the legend to horizontally center the legend in the remaining space.
Here's my code:
library(ggplot2)
# Generate dummy data
x <- paste0("var", seq(1,10))
y <- LETTERS[1:10]
data <- expand.grid(X=x, Y=y)
data$Z <- runif(100, -2, 2)
# Generate heatmap WITHOUT fixed aspect ratio. I address this below
p1 <- ggplot(data, aes(X, Y, fill= Z)) +
geom_tile() +
labs(title = 'A Heatmap Graph')
# Extract the legend from our plot
legend = gtable::gtable_filter(ggplotGrob(p1), "guide-box")
plot_output <- gridExtra::arrangeGrob(
p1 + theme(legend.position="none"), # Remove legend from base plot
grid::rectGrob(gp=grid::gpar(col=NA)), # Add a spacer
legend, # Add the legend back
grid::rectGrob(gp=grid::gpar(col=NA)), # Add a spacer
nrow=1, # Format plots in 1 row
widths=grid::unit.c(unit(0.5, "npc"), # Plot takes up half of width
(unit(0.5, "npc") - legend$width) * 0.5, # Spacer width
legend$width, # Legend width
(unit(0.5, "npc") - legend$width) * 0.5)) # Spacer width
# A text grob for the footer
figure_number_grob <- grid::textGrob('Figure 10',
x = 0.004,
hjust = 0,
gp = grid::gpar(fontsize = 10,
col = '#01A184'))
plot_grid <- ggpubr::ggarrange(plot_output,
figure_number_grob,
ncol = 1,
nrow = 2,
heights = c(1,
0.05))
# save it
png(filename = '~/test.png', width = 3000, height = 1500, res = 300, type = 'cairo')
print(plot_grid)
dev.off()
And here's the output image: https://i.stack.imgur.com/rgzFy.png

Absolute positioning of rasterGrobs in gtable cells

I have been attempting to specify absolute positions for rasterGrobs in gtable cells without success. I would like to be able to have the extents of an image align to values on the y axis. The script aligns drill-core images alongside multi-sensor data plotted in ggplot2 facets. For example, a particular radiograph core image needs to have its top at 192 mm, and bottom at 1482 mm, but I want the scale to go from 0 to 1523 mm. Please see the included link for an example of what I am doing, but for simplicity I have only posted an MWE here. Is it possible to specify an absolute position for a rasterGrob inside a gtable cell?
sample of intended output
In terms of the MWE below, my only solution thus far has been to move Rlogo.png around using relative positions set when using rasterGrob(). Using "native" coordinates does not appear to be what I need either. Similarly, I can't make sense of the position parameters called in gtable_add_grob().
library(png)
library(ggplot2)
library(gtable)
# read Image
img <- readPNG(system.file("img", "Rlogo.png", package = "png"))
# convert to rastergrob
g <- rasterGrob(img, y = unit(0.5, "npc"), x = unit(0.5, "npc"))
# create plot
tp <- qplot(1:5, 1:5, geom="blank") + scale_y_reverse()
# convert plot to gtable
tt <- ggplot_gtable(ggplot_build(tp))
# add column to gtable to hold image
tt <- gtable_add_cols(tt, tt$width[[.5*4]], 3)
# add grob to cell 3, 4
tt <- gtable_add_grob(tt,g,3,4)
# render
grid.draw(tt)
Did a lot of searching before coming up with this solution of using rasterGrob to add images to panels in a ggplot. Perhaps though there is a more elegant solution someone can suggest?
The grob can set its position within a cell, as illustrated below
library(gridExtra)
library(grid)
library(gtable)
# quick shortcut to create a 2x2 gtable filled with 4 rectGrobs
tg <- arrangeGrob(grobs=replicate(4, rectGrob(), FALSE))
# red rect of fixed size with default position (0.5, 0.5) npc
rg1 <- rasterGrob("red", width=unit(1,"cm"), height=unit(1,"cm"))
# blue rect with specific x position (0) npc, left-justified
rg2 <- rasterGrob("blue", width=unit(1,"cm"), height=unit(1,"cm"),
x = 0, hjust=0)
# green rect at x = 1cm left-justified, y=-0.5cm from middle, top-justified
rg3 <- rasterGrob("green", width=unit(1,"cm"), height=unit(1,"cm"),
x = unit(1,"cm"), y=unit(0.5, "npc") - unit(0.5, "cm"),
hjust=0, vjust=1)
# place those on top
tg <- gtable_add_grob(tg, rg1, 1, 2, z = Inf, name = "default")
tg <- gtable_add_grob(tg, rg2, 1, 2, z = Inf, name = "left")
tg <- gtable_add_grob(tg, rg3, 1, 2, z = Inf, name = "custom")
grid.newpage()
grid.draw(tg)

How to put text at the top of subplots in R

If you don't know how to put text (using the text function so it can be more freely edited, not the legend function) at the top of each subplot in R when the coordinates vary and you don't know the y max or x max (e.g. for a histogram where you don't know the frequency in advance) how do you do it?
Addendum: Also, mtext uses margins, I am speaking of coordinate space here, not margin space.
You can use par ('usr'). It gives you the current coordinates of the plot. Really useful if you want text in a certain area of each subplot and you don't know the coordinates in advance (e.g. a histogram). The output looks like:
par('usr')
[1] -0.28 7.28 -3.00 78.00
wherein the x min is the first member of the list, the x max is the second, the y min is the third, and the y max is the fourth. You can treat par('usr') like a vector in R and if you want it to be in the top left you can do, say:
text(par('usr')[1]+2,.9*par('usr')[4],labels="blahblah")
From this it will be plotted in the upper 10% of the plot and +2 from the leftmost coordinate of the plot space. Of course you can adjust this, but that would be top left more or less.
Using this code for my data:
y <- rnorm(100)
z <- rnorm(100)
par(mfrow = c(1,2))
hist(y, breaks = 30)
text(.8 * par('usr')[2], .9 * par('usr')[4], labels = paste("mean:", round(mean(y), 2)))
text(.8 * par('usr')[2], .86 * par('usr')[4], labels = paste("median:", round(median(y), 2)))
hist(z, breaks = 30)
text(.8 * par('usr')[2], .9 * par('usr')[4], labels = paste("mean:", round(mean(z), 2)))
text(.8 * par('usr')[2], .86 * par('usr')[4], labels = paste("median:", round(median(z), 2)))
mtext("distributions", side = 3, line = -2, outer = TRUE, col = 2) # added mtext to show how I would use it to create a title
I got this image:
Assuming you want the text inside the plot region, you can use legend, and specify the position by keyword (see details in ?legend):
par(mfrow=c(2, 2))
sapply(1:4, function(i) {
plot(runif(10))
legend('top', paste('Plot', i), bty='n', text.font=2)
})
You could also use mtext:
mtext(paste('Plot', i), 3, line = -1.5)

R, layout: How to determine the width of the parts of a plotting region in order to center a title?

I would like to center a title with respect to a plot matrix (and not the overall plot) I created with this code:
d <- 3
d2 <- d*d
layout.mat <- matrix(1:d2, byrow=TRUE, ncol=d) # plot matrix
layout.mat <- cbind(layout.mat, rep(0, d)) # space
layout.mat <- cbind(layout.mat, rep(d2+1, d)) # column on the right side
wspace <- 6*par("csi")*2.54 # width of the space in character height in cm
wside <- 3*par("csi")*2.54 # width of the right side in character height in cm
layout(layout.mat, respect=TRUE, widths=c(rep(1, d), lcm(wspace), lcm(wside)))
layout.show(d2+1)
par(mar=rep(0, 4), oma=c(4,4,6,4))
for(i in 1:d){
for(j in 1:d){
plot.new()
plot.window(xlim=c(0,1), ylim=c(0,1))
ll <- par("usr")
rect(ll[1], ll[3], ll[2], ll[4])
text(0.5, 0.5, paste("i=",i,", j=",j,sep=""), cex=1.4)
}
}
plot.new()
plot.window(xlim=c(0,1), ylim=c(0,1))
ll <- par("usr")
rect(ll[1], ll[3], ll[2], ll[4])
text(0.5, 0.5, "side", cex=1.4)
## title
mtext("This title should be centered according to the plot matrix", side=3,
line=2, outer=TRUE, adj=0.5, at=0.5)
How can I determine (with given fixed distances wspace and wside) the width of plot region so that I can subtract wspace+wside from it to determine the width of the plot matrix? The ultimate goal is to determine a precise value for adj of mtext such that the title is centered above the plot matrix and not the overall plotting region.
How about adding this code inside your for loop:
if (i == 1 & j == 2){
mtext("This title should be centered according to the plot matrix", side=3,
line=2, outer=FALSE, adj=0.5, at=0.5)
}
Note the change to outer = FALSE. Here's what it looks like, with shorter title text (the long version runs off either side in a small plot, but is still centered):
Of course, this hack only works if you have an odd number of columns. A more general solution may be to alter the layout to include a short, wide horizontal region at the top, and plot text in that region directly:
d <- 4
d2 <- d*d
layout.mat <- matrix(1:d2, byrow=TRUE, ncol=d) # plot matrix
layout.mat <- cbind(layout.mat, rep(0, d)) # space
layout.mat <- cbind(layout.mat, rep(d2+1, d)) # column on the right side
layout.mat <- rbind(c(rep(18,d),0,0),layout.mat) #Add row on top
wspace <- 6*par("csi")*2.54 # width of the space in character height in cm
wside <- 3*par("csi")*2.54 # width of the right side in character height in cm
#Note adjustments to heights
layout(layout.mat, respect=TRUE, widths=c(rep(1, d), lcm(wspace), lcm(wside)),
heights = c(0.25,rep(1,nrow(layout.mat)-1)))
layout.show(d2+1)
par(mar=rep(0, 4), oma=c(4,4,6,4))
for(i in 1:d){
for(j in 1:d){
plot.new()
plot.window(xlim=c(0,1), ylim=c(0,1))
ll <- par("usr")
rect(ll[1], ll[3], ll[2], ll[4])
text(0.5, 0.5, paste("i=",i,", j=",j,sep=""), cex=1.4)
}
}
plot.new()
plot.window(xlim=c(0,1), ylim=c(0,1))
ll <- par("usr")
rect(ll[1], ll[3], ll[2], ll[4])
text(0.5, 0.5, "side", cex=1.4)
## title
plot.new()
plot.window(xlim=c(0,1), ylim=c(0,1))
ll <- par("usr")
#rect(ll[1], ll[3], ll[2], ll[4])
text(0.5, 0.5, "top", cex=1.4)
You may want to look at the grconvertX and grconvertY functions. These can be used to convert between different coordinate systems. You could use this to find the device coordinates corresponding to the right and left edges of the plotting or figure regions, then use that information to decide where to place the overall title.
Here is an example of one way to do it. The nice part about this is it can be run after any of the plotting commands (you don't need to create and set up the final plot region at the top but rather put the text into the margin area.
# left edge of matrix
l.x <- grconvertX(0, from='nic')
# right side of matrix (right side of inner minus wspace and wside)
r.x <- grconvertX(grconvertX(1, from='nic', to='inches') - (wspace+wside)/2.54,
from='inches')
# find 2 line heights from top
t.y <- grconvertY(1, from='ndc') - 2*strheight('Test text')
# or 2 line heights above inner margin
t.y <- grconvertY(1, from='nic') + 2*strheight('Test text')
par(xpd=NA)
text( (l.x+r.x)/2, t.y, 'Test text' )

Resources