Show a grid of equally spaced images with layout() and display() - r

I want to create a grid of 9 images with equal spacing between them. Until now I managed to get something to work with par() and layout().
layout(matrix(1:9, widths=rep(lcm(4),9), heights=rep(lcm(3),9))
for (i in 1:9) {
imNew <- readImage(img_ar[i])
EBImage::display(imNew, method="raster")
}
gives me this
while using par:
layout(matrix(1:9, widths=rep(lcm(4),9), heights=rep(lcm(3),9))
for (i in 1:9) {
imNew <- readImage(img_ar[i])
EBImage::display(imNew,method="raster")
}
gives me this:
I also tried different options for par() like oma, mai and mar but these didn't change the spacing in between the individual images. What I like to have is an equal distance between the individual images like this:
Could anyone help me please?

You can use your original idea to separate images by including additional empty rows and columns in your layout, as in the following example. Note, however, that in order to achieve equal horizontal and vertical spacing you will need to tweak the device dimensions.
library(EBImage)
# load sample image
img <- readImage(system.file("images", "sample-color.png", package="EBImage"))
# downsample to reduce memory consumption and for faster processing
img <- resize(img, 192)
# build the layout matrix with additional separating cells
nx <- 4 # number of images in a row
ny <- 3 # number of images in a column
cols <- 2*nx-1
rows <- 2*ny-1
m <- matrix(0, cols, rows)
m[2*(1:nx)-1, 2*(1:ny)-1] <- 1:(nx*ny)
m <- t(m)
# relative spacing
pad <- .1
w <- rep(1, cols)
w[!(1:cols)%%2] <- pad
h <- rep(1, rows)
h[!(1:rows)%%2] <- pad * dim(img)[1L]/dim(img)[2L]
layout(m, widths = w, heights = h)
layout.show(nx*ny)
for (i in 1:(nx*ny)) {
display(img, method="raster")
}
A better approach is to use display() on an image stack. Then individual frames can be displayed arranged in a grid by setting all=TRUE.
## construct sample image stack
img_stack <- combine(replicate(nx*ny, img, simplify=FALSE))
display(img_stack, method="raster", all=TRUE)
Unfortunately, until recently it was not possible to adjust the spacing between the frames. To facilitate this, I've added an argument to display() specifying the spacing. Currently this new feature is available in the development version of EBImage, which can be obtained either from GitHub devtools::install_github("aoles/EBImage"), or from the Bioconductor devel branch.
The spacing can be provided as a fraction of frame dimensions (positive numbers <1) or in pixels (numbers >=1). Additionally, you can have different horizontal and vertical separation by providing a vector, e.g. spacing = (10, 20) will separate the columns by 10px, and the rows by 20px.
display(img_stack, method="raster", all=TRUE, spacing=.1)
Furthermore, you can add a margin around the grid, and control its layout by nx. The background can be set through bg passed to par().
Finally, a completely different way of drawing images in a grid is to construct one big composite image with tile. This approach might be useful, for example, when saving the result to a file. Note the additional border around the whole grid.
## tiled composite image
img_tiles <- tile(img_stack, nx=nx, lwd=20, fg.col="white", bg.col="white")
display(img_tiles, method="raster")

The ggplot2 way with marrangeGrob() from gridExtra:
library(RCurl)
library(png)
library(grid)
library(gridExtra)
library(ggplot2)
# read a few MNIST images
urls <- c('https://i.imgur.com/TEbkTqu.png', 'https://i.imgur.com/tnsjMFJ.png', 'https://i.imgur.com/VUZgJBs.png', 'https://i.imgur.com/FZ28d3w.png')
imgs <- list()
for (i in 1:length(urls)) {
imgs[[i]] <- readPNG(getURLContent(urls[i]))
}
# plot grid and show images
plist <- list()
for (i in 1:length(imgs)) {
plist[[i]] <- ggplot() +
annotation_custom(rasterGrob(imgs[[i]], interpolate=TRUE), xmin=-Inf, xmax=Inf, ymin=-Inf, ymax=Inf) +
labs(x = NULL, y = NULL) +
guides(x = "none", y = "none") +
theme_bw() +
theme(legend.position = "none", panel.border = element_blank(), panel.grid.major = element_blank(),
panel.grid.minor = element_blank())
}
marrangeGrob(plist, nrow=2, ncol=2, respect=TRUE)
(Demonstrated here in the shiny app too)

Related

Is it possible to plot images in a ggplot2 plot, that don't get distorted when you save to any non-standard aspect ratio?

I'm looking for any solution to this problem, regardless of packages used.
The problem at hand is that plotted images get distorted when you save them using ggsave. Let me give an example:
image_links = data.frame(id = c(1,2,3,4,5),
image = c("https://cdn.shopify.com/s/files/1/1061/1924/products/Smiling_Emoji_with_Eyes_Opened_large.png",
"https://cdn.shopify.com/s/files/1/1061/1924/products/Smiling_Emoji_with_Smiling_Eyes_large.png",
"https://cdn.shopify.com/s/files/1/1061/1924/products/Hushed_Face_Emoji_large.png",
"https://cdn.shopify.com/s/files/1/1061/1924/products/Disappointed_but_Relieved_Face_Emoji_large.png",
"https://cdn.shopify.com/s/files/1/1061/1924/products/Expressionless_Face_Emoji_large.png"))
mydata = data.frame(x = rnorm(100, mean = 50, sd = 20),
y = rnorm(100, mean = 50, sd = 5),
id = rep(c(1,2,3,4,5), 20))
mydata$y = mydata$y - 10*mydata$id
mydata = mydata %>% left_join(image_links, by='id')
g <- ggplot(mydata) + geom_image(aes(x=x, y=y, image=image), size=0.05)
ggsave(g, filename='[INSERT PATH HERE].png', width=width, height=height, dpi=300)
This works fine:
The problem arises when you adjust the width and height parameters of ggsave, for instance because you want the x and y-axis to be in the correct proportion:
width = (max(mydata$x) - min(mydata$x))/10
height = (max(mydata$y) - min(mydata$y))/10
ggsave(g, filename='[INSERT PATH HERE].png', width = width, height=height, dpi=300)
The x and y-axis are now fine, but the images are distorted:
This happens in ANY situation where you plot an image but the width/height aspect ratio is different than what was the original aspect ratio of the image you want to add.
I'm looking for any solution to this problem, not necessarily restricted to ggimage. It seems very weird to me that you can't properly add images to a ggplot, as I image it's very common for people to want to do that.
I don't know a lot about ggsave, but this seems like an issue related to relative versus absolute units. Probably the geom_image() calculates positions relative to the axes, which get distorted when the axes get resized (such as within ggsave). For example:
ggplot(mydata) + geom_image(aes(x=x, y=y, image=image), size=0.05)
Can look like:
Or can look like:
Depending on the device window that I can resize at will.
There are two ways I can see this getting fixed, both of which will involve re-calculating the sizes of the rasters at drawtime. The easier fix will be the one below.
# Get plot
g <- ggplot(mydata) + geom_image(aes(x=x, y=y, image=image), size=0.05)
# Convert to gtable
gt <- ggplotGrob(g)
# Get the imagegrobs, correct slots found by trial and error
imagegrobs <- gt$grobs[[6]]$children[[3]]$children
# Re-class them to a custom, made-up class
imagegrobs <- lapply(imagegrobs, function(image) {
class(image) <- c("fixasp_raster", class(image))
image
})
# Put them back into the gtable
gt$grobs[[6]]$children[[3]]$children <- imagegrobs
So now that we have a custom class for these images, we can write a piece of code that gets executed at the time of drawing by writing a method for our class using the S3 generic makeContent from the grid package.
library(grid)
makeContent.fixasp_raster <- function(x) {
# Convert from relative units to absolute units
h <- convertHeight(x$height, "cm", valueOnly = TRUE)
w <- convertWidth(x$width, "cm", valueOnly = TRUE)
# Decide how the units should be equal
x$height <- x$width <- unit(sqrt(h * w), "cm")
x
}
Note that taking the square root of the product is improvised, I don't know if this is the optimal procedure.
When we plot the data now, we'll have a consistent size of the images, regardless of the aspect ratio:
grid.newpage(); grid.draw(gt)
The second way to fix this is to file an issue in the github page of the ggimage package, motivating your use case and convice them to implement something that adresses your concerns. If they want, they could make a fix at the ggproto level, so that you don't have dabble with gtables.
#teunbrand's answer has been implemented in dev version 0.2.4 of ggimage. You can install the latest dev version like this:
setRepositories(ind=1:2)
## install.packages("devtools")
devtools::install_github("GuangchuangYu/ggimage")
This should fix the aspect ratio issues.

Convert base graphics to their grid equivalent in a loop

Converting a base graph one by one to their grid equivalent using the solution with gridGraphics package (here) works without issues. However, when I try to put that in a loop I do not get the expected. Here is an example:
library(gridGraphics)
### Make a list of 3 base plots -----
p <- vector(mode = "list", length = 3)
for (i in 1:3){
plot(1:5^i)
p[[i]] <- recordPlot()
}
### Attempt to convert each base plot to its grid equivalent -----
grobs <- vector(mode = "list", length = 3)
for (i in 1:3){
plot.new() # clean up device
p[[i]] # redraw
# Echo graphics output using grid graphics
grid.echo()
# Creates a gTree object from the current grid display list
a_gTree <- grid.grab()
grobs[[i]] <- editGrob(grob = a_gTree,
vp = viewport(width = unit(5, "cm"),
height = unit(5, "cm"),
angle = 90)) # rotates 90 dg
}
If I run the chunk of code inside the loop for each step I get what I need, but when I run the loop in one shot, then all grobs seems to display nothing. I feel that there is something obvious that I'm missing ...
Here is a desired output (obtained by running step by step):
cowplot::plot_grid(grobs[[1]],
grobs[[2]],
grobs[[3]])
Thanks to #user20650 to pointing out the usage of print() in the loop, so using print(p[[i]]) instead of p[[i]]. Or even better, I prefer his elegant suggestion to save some lines with using a_gTree <- grid.grabExpr(grid.echo(p[[i]])). Where grid.grabExpr captures the output from an expression without drawing anything. Also plot.new() seems optional.
for (i in 1:3){
# Grab grid output
a_gTree <- grid.grabExpr(grid.echo(p[[i]]))
# Edit/modify the grob
grobs[[i]] <- editGrob(grob = a_gTree,
vp = viewport(width = unit(5, "cm"),
height = unit(5, "cm"),
angle = 90)) # rotates 90 dg
}

Align text to edge of grid R

I made a grid, I drew some lines between each cell.
I want text in the upper lefthand corner of a cell, but I can only get alignment relative to the cell's center.
I've spent a lot of time searching, and this is the closest I could find is What do hjust and vjust do when making a plot using ggplot?. The [0,1] values of hjust and vjust align the text in reference to the points in this example, and on a grid (using grid.text) they align the text relative to the center of the cell. I've tried values of hvjust outside of [0,1] with no luck; I've tried using decimal places when specifying the row/column (1.5 should be between row 1 and 2 right?) for text placement, but the decimals just get rounded. I can't align by hand because my script should be aligning many names of variable length.
Code to layout the grid:
grid.newpage()
pushViewport(viewport(layout=grid.layout(29+1,7, heights = unit(rep(1,
(29+1), "null"), widths=unit(c(1,.5,.5,.5,1,1,1), "null"))))
grid.polyline(x=c(0,0,1,1,1.5,1.5,2,2,2.5,2.5,3.5,3.5,4.5,4.5,5.5,5.5)/5.5, y=rep(c(0,1), 8), id.lengths=rep(2,8))
grid.polyline(x=rep(c(0,1), 29+2), y=sort(rep(0:(29+1), 2)/(29+1)), id.lengths=rep(2,29+2))
Assume the 29s are variable numbers, I had to change them from something more specific. If the code doesn't layout the grid, I must've deleted an extra parenthesis. The only other code I have just places text in the near center of a cell of the grid.
My goal is to place text so that the first letter is in the upper left corner next to the gridlines. Any guidance is greatly appreciated.
You may find it easier to work with a gtable, in which case each label can be centred within its own cell. Otherwise, with your strategy, you'd need to keep track of all x and y positions (or define as many individual viewports, but that's essentially what gtable does on top of grid).
Here's an example (I used tableGrob to set up the gtable only for convenience),
library(gtable)
library(gridExtra)
g <- tableGrob(matrix("", 29+1, 7), theme = ttheme_minimal())
g$widths <- unit(c(1,.5,.5,.5,1,1,1), "null")
g$heights <- unit(rep(1, (29+1)), "null")
vs <- replicate(ncol(g)-1,
segmentsGrob(x1 = unit(0, "npc"), gp=gpar(lwd=0.5)),
simplify=FALSE)
hs <- replicate(nrow(g)-1,
segmentsGrob(y1 = unit(0, "npc"), gp=gpar(lwd=0.5)),
simplify=FALSE)
g <- gtable::gtable_add_grob(g, grobs = vs,
t = 1, b = nrow(g), l = seq_len(ncol(g)-1)+1)
g <- gtable::gtable_add_grob(g, grobs = hs,
l = 1, r = ncol(g), t = seq_len(nrow(g)-1))
labels <- list(textGrob("l", hjust=0, x=0),
textGrob("c", hjust=0.5, x=0.5),
textGrob("r", hjust=1, x=1))
g <- gtable_add_grob(g, labels, t=c(1,2,3), l=c(2, 4, 7), z = -1)
grid.newpage()
grid.draw(g)
Note that this strategy is however very inefficient, because it doesn't make use of vectorised grobs (instead individual grobs are placed). For a small enough table it may not matter, and the gain in convenience can be worthwhile. See this wiki page for further examples of gtable processing.

combine multiple plots to a gif

Im trying to use the caTools package to combine multiple plots into a gif.
My basic code looks like :
for( i in 1:100){
plot(....) // plots few points and lines, changes slightly with each i
}
I would like to combine these to a gif to see the "evolution" of the plot.
However for write.gif() from caTools, I need to give an image as an input.
For each i, how do I convert the plot into an image without
saving to disc as an intermediate step
depending on ImageMagick or similar external dependencies.
Please feel free to point out if this is a duplicate. [ Creating a Movie from a Series of Plots in R doesnt seem to answer this ]
EDIT: Basically this requires us to convert a plot to a matrix. Since this very likely happens every time someone saves a plot, it should not be very difficult. However Im not able to get hold of how to exactly do this.
I suggest using the animation package and ImageMagick instead:
library(animation)
## make sure ImageMagick has been installed in your system
saveGIF({
for (i in 1:10) plot(runif(10), ylim = 0:1)
})
Otherwise you could try it in the veins of this (plenty of room for optimization):
library(png)
library(caTools)
library(abind)
# create gif frames and write them to pngs in a temp dir
dir.create(dir <- tempfile(""))
for (i in 1:8) {
png(file.path(dir, paste0(sprintf("%04d", i), ".png")))
plot(runif(10), ylim = 0:1, col = i)
dev.off()
}
# read pngs, create global palette, convert rasters to integer arrays and write animated gif
imgs <- lapply(list.files(dir, full.names = T), function(fn) as.raster(readPNG(fn)))
frames <- abind(imgs, along = 3) # combine raster pngs in list to an array
cols <- unique(as.vector(frames)) # determine unique colors, should be less then 257
frames <- aperm(array(match(frames, cols) - 1, dim = dim(frames)), c(2,1,3)) # replace rgb color codes (#ffffff) by integer indices in cols, beginning with 0 (note: array has to be transposed again, otherwise images are flipped)
write.gif(
image = frames, # array of integers
filename = tf <- tempfile(fileext = ".gif"), # create temporary filename
delay = 100, # 100/100=1 second delay between frames
col = c(cols, rep("#FFFFFF", 256-length(cols))) # color palette with 256 colors (fill unused color indices with white)
)
# open gif (windows)
shell.exec(tf)
Is that what you are looking for?
library(ggplot2)
a <- 0:10
df <- data.frame(a=a,b=a)
steps <-function(end){
a <- ggplot(df[1:end,], aes(a,b)) +
geom_point() +
scale_x_continuous(limits=c(0,10)) +
scale_y_continuous(limits=c(0,10))
print(a)
}
gif <- function() {
lapply(seq(1,11,1), function(i) {
steps(i)
})
}
library(animation)
saveGIF(gif(), interval = .2, movie.name="test.gif")
I liked #ttlngr's answer but I wanted something a bit simpler (it still uses ImageMagick).
saveGIF({
for (i in 1:10){
a <- ggplot(df[1:i,], aes(a,b)) +
geom_point() +
scale_x_continuous(limits=c(0,10)) +
scale_y_continuous(limits=c(0,10))
print(a)}
}, interval = .2, movie.name="test.gif")

Get width of plot area in ggplot2

Is there any way to get the width of the plot area in the grid window? It grows or shrinks, for instance, if plot.margin is changed or if the y-axis labels' font-size is increased. Is is hidden somewhere in str(p)?
Any size measure would work. I need to be able to measure the relative change in the width of the plot area in different scenarios such as change of y-axis labels' font-size.
df = data.frame(x = (1:3),One=c(12, 8, 13),Two=c(13, 7, 11),Three=c(11, 9, 11))
df.melt = melt(df, id.vars="x")
p = ggplot(df.melt, aes(x=x, y=value, color=variable)) +
geom_line() +
coord_cartesian(xlim=c(min(df.melt$x),max(df.melt$x))) +
theme(legend.position="none", plot.margin = unit(c(1, 4, 1, 1), "cm"))
p
UPDATE – To clarify: Please help me calculate a/b.
p = ggplot(df.melt, aes(x=x, y=value, color=variable)) +
geom_line() + coord_cartesian(xlim=c(min(df.melt$x),max(df.melt$x))) +
theme(legend.position="none")
p1 = p + theme(plot.margin=unit(c(1,1,1,1),"cm"), axis.text.y=element_text(size=10))
p2 = p + theme(plot.margin=unit(c(1,1,1,2),"cm"), axis.text.y=element_text(size=30))
grid.arrange(p1, p2, ncol=2)
The plot in ggplot2 uses grid graphics. A graphical scene that has been produced
using the grid graphics package consists of grobs and viewports.
You can use gridDebug package for the inspection of the grobs.
showGrob show the locations and names of the grobs used to draw the scene
showGrob()
Get the gpath of the grob
sceneListing <- grid.ls(viewports=T, print=FALSE)
do.call("cbind", sceneListing)
name gPath
[1,] "ROOT" ""
[2,] "GRID.gTableParent.45019" ""
[3,] "background.1-5-6-1" "GRID.gTableParent.45019"
[4,] "spacer.4-3-4-3" "GRID.gTableParent.45019"
[5,] "panel.3-4-3-4" "GRID.gTableParent.45019"
[6,] "grill.gTree.44997" "GRID.gTableParent.45019::panel.3-4-3-4"
Retrieve the gorb
h <- grid.get(gPath="GRID.gTableParent.45019")
get h properties (e.g)
h$layoutvp$width
Application:
grid.get('x',grep=TRUE,global=T)
(polyline[panel.grid.minor.x.polyline.21899], polyline[panel.grid.major.x.polyline.21903], gTableChild[axis-l.3-3-3-3], gTableChild[axis-b.4-4-4-4], gTableChild[xlab.5-4-5-4])
> grid.get('x',grep=TRUE,global=T)[[3]]
gTableChild[axis-l.3-3-3-3]
> xx <- grid.get('x',grep=TRUE,global=T)[[3]]
> grobWidth(xx)
[1] sum(1grobwidth, 0.15cm+0.1cm)
This intrigued me enough to look into it deeper. I was hoping that the grid.ls function would give the information to navigate to the correct viewports to get the information, but for your example there are a bunch of the steps that get replaced with '...' and I could not see how to change that to give something that is easily worked with. However using grid.ls or other tools you can see the names of the different viewports. The viewports of interest are both named 'panel.3-4-3-4' for your example, below is some code that will navigate to the 1st, find the width in inches, navigate to the second and find the width of that one in inches.
grid.ls(view=TRUE,grob=FALSE)
current.vpTree()
seekViewport('panel.3-4-3-4')
a <- convertWidth(unit(1,'npc'), 'inch', TRUE)
popViewport(1)
seekViewport('panel.3-4-3-4')
b <- convertWidth(unit(1,'npc'), 'inch', TRUE)
a/b
I could not figure out an easy way to get to the second panel without poping the first one. This works and gives the information that you need, unfortunately since it pops the 1st panel off the list you cannot go back to it and find additional information or modify it. But this does give the info you asked for that could be used in future plots.
Maybe someone else knows how to navigate to the second panel without popping the first, or getting the full vpPath of each of them to navigate directly.
This answer is mainly in reply to comments by #java_xof. The reply is too long and includes code so it will not fit in a comment. However, it may help with the original question as well (or at least give a starting place).
Here is a function and some code using it (it requires the tcltk and tkrplot packages):
library(ggplot2)
library(tkrplot)
TkPlotLocations <- function(FUN) {
require(tkrplot)
cl <- substitute(FUN)
replot <- function() eval(cl)
tt <- tktoplevel()
img <- tkrplot(tt, replot, vscale=1.5, hscale=1.5)
tkpack(img)
tkpack(xfr <- tkframe(tt), side='left')
tkpack(yfr <- tkframe(tt), side='left')
xndc <- tclVar()
yndc <- tclVar()
xin <- tclVar()
yin <- tclVar()
tkgrid(tklabel(xfr, text='x ndc'), tklabel(xfr, textvariable=xndc))
tkgrid(tklabel(yfr, text='y ndc'), tklabel(yfr, textvariable=yndc))
tkgrid(tklabel(xfr, text='x inch'), tklabel(xfr, textvariable=xin))
tkgrid(tklabel(yfr, text='y inch'), tklabel(yfr, textvariable=yin))
iw <- as.numeric(tcl("image","width", tkcget(img, "-image")))
ih <- as.numeric(tcl("image","height",tkcget(img, "-image")))
cc <- function(x,y) {
x <- (as.real(x)-1)/iw
y <- 1-(as.real(y)-1)/ih
c(x,y)
}
mm <- function(x, y) {
xy <- cc(x,y)
tclvalue(xndc) <- xy[1]
tclvalue(yndc) <- xy[2]
tclvalue(xin) <- grconvertX(xy[1], from='ndc', to='inches')
tclvalue(yin) <- grconvertY(xy[2], from='ndc', to='inches')
}
tkbind( img, "<Motion>", mm)
invisible()
}
x <- runif(25)
y <- rnorm(25, x, 0.25)
plot(x,y)
par()$pin
par()$plt
TkPlotLocations(plot(x,y))
qplot(x,y)
par()$pin
par()$plt
TkPlotLocations(print(qplot(x,y)))
qplot(x,y) + xlab('Multi\nline\nx\nlabel')
par()$pin
par()$plt
TkPlotLocations(print(qplot(x,y) + xlab('Multi\nline\nx\nlabel')))
Defining the above function, then running the following lines will produce 3 plots of the same random data. You can see that the results of par()$pin and par()$plt (and other parameters) are exactly the same for the 3 plots even though the plotting regions differ in the plots.
There will also be 3 new windows that have popped up, in the windows you can move the mouse pointer over the graph and at the bottom of the window you will see the current location of the pointer in normalized device coordinates and in inches (both from the bottom left corner of the device region). You can hover the mouse pointer over the corners of the graph (or any other part) to see the values and compare between the 3 graphs.
This may be enough to answer at least part of the original question (just not programatically, which would be more useful). The functon can be modified to print out other measurements as well. I may expand this and include it in a package in the future if others would be interested.

Resources