26

First I thought I need to it manually in powerpoint, then I thought may be try with R, if there is a solution. Here is my example data:

set.seed(123)
myd<- expand.grid('cat' = LETTERS[1:5], 'cond'= c(F,T), 'phase' = c("Interphase", "Prophase", "Metaphase", "Anaphase", "Telophase"))
myd$value <- floor((rnorm(nrow(myd)))*100)
myd$value[myd$value < 0] <- 0

require(ggplot2)
ggplot() +
  geom_bar(data=myd, aes(y = value, x = phase, fill = cat), stat="identity",position='dodge') +
  theme_bw()

Here is what output should look like: enter image description here

The jpeg image can be randomly generated (to demo examples) or example figures at the links:

Interphase prophase , metaphase, anaphase , telophase

Edit:

Suggestion @bapste

enter image description here

Claus Wilke
  • 16,992
  • 7
  • 53
  • 104
jon
  • 11,186
  • 19
  • 80
  • 132
  • 2
    it should be a job for `annotation_raster` but it doesn't seem to work with a discrete axis, unfortunately. – baptiste Dec 28 '12 at 21:06

4 Answers4

14

You can create a custom element function for axis.text.x, but it's quite fiddly and convoluted. Similar requests have been made in the past, it would be nice to have a clean solution for this and other custom changes (strip labels, axes, etc.) Feature request, anyone?

enter image description here

library(jpeg)
img <- lapply(list.files(pattern="jpg"), readJPEG )
names(img) <- c("Anaphase", "Interphase", "Metaphase", "Prophase", "Telophase")

require(ggplot2)
require(grid)

# user-level interface to the element grob
my_axis = function(img) {
    structure(
      list(img=img),
      class = c("element_custom","element_blank", "element") # inheritance test workaround
    )
  }
# returns a gTree with two children: the text label, and a rasterGrob below
element_grob.element_custom <- function(element, x,...)  {
  stopifnot(length(x) == length(element$img))
  tag <- names(element$img)
  # add vertical padding to leave space
  g1 <- textGrob(paste0(tag, "\n\n\n\n\n"), x=x,vjust=0.6)
  g2 <- mapply(rasterGrob, x=x, image = element$img[tag], 
               MoreArgs = list(vjust=0.7,interpolate=FALSE,
                               height=unit(5,"lines")),
               SIMPLIFY = FALSE)

  gTree(children=do.call(gList,c(g2,list(g1))), cl = "custom_axis")
}
# gTrees don't know their size and ggplot would squash it, so give it room
grobHeight.custom_axis = heightDetails.custom_axis = function(x, ...)
  unit(6, "lines")

ggplot(myd) +
  geom_bar(aes(y = value, x = phase, fill = cat), stat="identity", position='dodge') +
  theme_bw() +
  theme(axis.text.x = my_axis(img),
          axis.title.x = element_blank())

ggsave("test.png",p,width=10,height=8)
baptiste
  • 75,767
  • 19
  • 198
  • 294
  • @bapsite thank you for the solution. I believe that the function applies to n number of categories . I would like to see alternative version if that gives better if we make 5 different plots rather a single grouped barplot ... – jon Dec 29 '12 at 15:14
  • 1
    @baptiste good result! but isn't a little complicated to get it? it seems that we need to to know in details how to build custom geoms/elements with ggplot2! – agstudy Dec 29 '12 at 16:51
  • @agstudy as it stands, it's probably not worth it; I'd do it in Illustrator. Food for thoughts though, I would like to see such custom elements more easily inserted into the ggplot2 framework. Lattice offers more flexibility in this regard, with every element being a function that can be overwritten by the user. – baptiste Dec 29 '12 at 23:40
  • 1
    @baptiste my approach is different. I assume we can't integrate all user customizations in neither framework, that why I take the best of lattice/ggplot2 to do a first draw, than I post process my draw with the base grid farmework. e.g I can use my the same code above with a lattice bwplot. I think this approach will be enhanced in R.2.16 with the new makeContent() hook. – agstudy Dec 30 '12 at 07:23
  • sure, this is an old answer. gtable certainly has the potential of solving many tricky ggplot2 questions, unfortunately its development stopped long ago. – baptiste Jul 06 '15 at 08:09
12

Using grid package, and playing with viewports, you can have this

enter image description here

## transform the jpeg to raster grobs
library(jpeg)
names.axis <-  c("Interphase", "Prophase", "Metaphase", "Anaphase", "Telophase")
images <- lapply(names.axis,function(x){
  img <- readJPEG(paste('lily_',x,'.jpg',sep=''), native=TRUE)
  img <- rasterGrob(img, interpolate=TRUE)
  img
  } )
## main viewports, I divide the scene in 10 rows ans 5 columns(5 pictures)
pushViewport(plotViewport(margins = c(1,1,1,1),
             layout=grid.layout(nrow=10, ncol=5),xscale =c(1,5)))
## I put in the 1:7 rows the plot without axis
## I define my nested viewport then I plot it as a grob.
pushViewport(plotViewport(layout.pos.col=1:5, layout.pos.row=1:7,
             margins = c(1,1,1,1)))
pp <- ggplot() +
  geom_bar(data=myd, aes(y = value, x = phase, fill = cat), 
                 stat="identity",position='dodge') +
  theme_bw()+theme(legend.position="none", axis.title.y=element_blank(),
                   axis.title.x=element_blank(),axis.text.x=element_blank())
gg <- ggplotGrob(pp)
grid.draw(gg)
upViewport()
## I draw my pictures in between rows 8/9 ( visual choice)
## I define a nested Viewport for each picture than I draw it.
sapply(1:5,function(x){
  pushViewport(viewport(layout.pos.col=x, layout.pos.row=8:9,just=c('top')))
  pushViewport(plotViewport(margins = c(5.2,3,4,3)))
  grid.draw(images[[x]])
  upViewport(2)
  ## I do same thing for text 
  pushViewport(viewport(layout.pos.col=x, layout.pos.row=10,just=c('top')))
  pushViewport(plotViewport(margins = c(1,3,1,1)))
    grid.text(names.axis[x],gp = gpar(cex=1.5))
  upViewport(2)
})
pushViewport(plotViewport(layout.pos.col=1:5, layout.pos.row=1:9,
             margins = c(1,1,1,1)))
grid.rect(gp=gpar(fill=NA))
upViewport(2)
agstudy
  • 119,832
  • 17
  • 199
  • 261
  • Thank you for the answer. May be I would try to organize it better - particularly align and put the label in axis as in my manual layout ... thank you for the idea again – jon Dec 28 '12 at 16:11
  • @jon it is a first attempt. it can be easily performed. – agstudy Dec 28 '12 at 16:13
  • @jon maybe it is better like this ? – agstudy Dec 28 '12 at 16:38
  • Yes, this is much better now !! thanks, I need to play with margins to make the photo bigger is that what you suggest ? – jon Dec 28 '12 at 16:50
  • 2
    @jon definitely yes !you need to play with margins! I change it. I think it looks good now. – agstudy Dec 28 '12 at 16:52
  • @agstudy, +1 for nice answer. Is is possible to print different photo, against OP has different figure different phases. How can we force appropriate order that match with category in barplots – SHRram Dec 28 '12 at 19:01
  • 3
    @SHRram I upadte my answer with a dummy list of images. The order is classic since you manipulate 2 lists (images and names.axis) – agstudy Dec 28 '12 at 20:38
  • There is now a fairly easy and powerful approach available using the `axis_canvas()` function in cowplot. I updated my earlier answer accordingly: https://stackoverflow.com/a/30765590/4975218 – Claus Wilke Nov 24 '17 at 20:45
8

I would now do this with the ggtext package. This is conceptually similar to the solution suggested here but with the hard work done in the package.

library(tidyverse)
library(ggtext)

set.seed(123)

data <- expand.grid(
  cat = LETTERS[1:5],
  cond= c(FALSE, TRUE),
  phase = c("Interphase", "Prophase", "Metaphase", "Anaphase", "Telophase")
) %>%
  mutate(
    value = floor(rnorm(n())*100),
    value = ifelse(value < 0, 0, value)
  )

# images from: http://www.microbehunter.com/mitosis-stages-of-the-lily/

labels <- c(
  Interphase = "<img src='img/interphase.jpg' width='60' /><br>Interphase",
  Prophase = "<img src='img/prophase.jpg' width='60' /><br>Prophase",
  Metaphase = "<img src='img/metaphase.jpg' width='60' /><br>Metaphase",
  Anaphase = "<img src='img/anaphase.jpg' width='60' /><br>Anaphase",
  Telophase = "<img src='img/telophase.jpg' width='60' /><br>Telophase"
)

ggplot(data, aes(phase, value, fill = cat)) +
  geom_col(position = "dodge") +
  scale_x_discrete(name = NULL, labels = labels) +
  theme(axis.text.x = element_markdown(lineheight = 1.2))

Created on 2020-01-29 by the reprex package (v0.3.0)

Claus Wilke
  • 16,992
  • 7
  • 53
  • 104
7

Note: I would now recommend the approach described here. It is more principled and simpler to understand.

Generating such a figure has become relatively straightforward with functions available in the cowplot package, specifically the axis_canvas() and insert_xaxis_grob() functions. (Disclaimer: I'm the package author.)

require(cowplot)

# create the data
set.seed(123)
myd <- expand.grid('cat' = LETTERS[1:5], 'cond'= c(F,T), 'phase' = c("Interphase", "Prophase", "Metaphase", "Anaphase", "Telophase"))
myd$value <- floor((rnorm(nrow(myd)))*100)
myd$value[myd$value < 0] <- 0

# make the barplot
pbar <- ggplot(myd) +
  geom_bar(aes(y = value, x = phase, fill = cat), stat="identity", position='dodge') +
  scale_y_continuous(limits = c(0, 224), expand = c(0, 0)) +
  theme_minimal(14) +
  theme(axis.ticks.length = unit(0, "in"))

# make the image strip
pimage <- axis_canvas(pbar, axis = 'x') + 
  draw_image("http://www.microbehunter.com/wp/wp-content/uploads/2009/lily_interphase.jpg", x = 0.5, scale = 0.9) +
  draw_image("http://www.microbehunter.com/wp/wp-content/uploads/2009/lily_prophase.jpg", x = 1.5, scale = 0.9) +
  draw_image("http://www.microbehunter.com/wp/wp-content/uploads/2009/lily_metaphase2.jpg", x = 2.5, scale = 0.9) +
  draw_image("http://www.microbehunter.com/wp/wp-content/uploads/2009/lily_anaphase2.jpg", x = 3.5, scale = 0.9) +
  draw_image("http://www.microbehunter.com/wp/wp-content/uploads/2009/lily_telophase.jpg", x = 4.5, scale = 0.9)

# insert the image strip into the bar plot and draw  
ggdraw(insert_xaxis_grob(pbar, pimage, position = "bottom"))

enter image description here

I'm reading the images straight from the web here, but the draw_image() function will also work with local files.

In theory, it should be possible to draw the image strip using geom_image() from the ggimage package, but I couldn't get it to work without having distorted images, so I resorted to five draw_image() calls.

Claus Wilke
  • 16,992
  • 7
  • 53
  • 104