Images as x-axis labels (updated)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
They say “if you want to find an answer on the internet, just present a wrong one as fact. Then wait.“
It didn’t take long, actually. Despite my searches while trying to get images into x-axis labels it seems I overlooked a working, significantly less hacky implementation. My Google-fu had in fact let me down.
Baptiste Auguié (@tpab / @baptiste) had this working a while ago (seemingly before the ggplot2
update that broke other methods), and in a definitively less hacky way. I’ve added a new gist (if you’re reading this on R-bloggers, the gist isn’t embedded, so either follow the link or view on my site) which implements it on the same graph as earlier, and I like this significantly more.
library(ggplot2) ## devtools::install_github("hadley/ggplot2) | |
library(grid) ## rasterGrob | |
library(EBImage) ## readImage | |
library(ggthemes) ## theme_minimal | |
## ########## | |
## INDEPENDENT CODE TO BE SOURCED: | |
## ########## | |
# 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(3,"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") | |
## ########## | |
## END | |
## ########## | |
## ########## | |
## OBTAIN FLAGS: | |
## ########## | |
library(rvest) | |
## GDP per capita, top 10 countries | |
url <- "https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)_per_capita" | |
html <- read_html(url) | |
gdppc <- html_table(html_nodes(html, "table")[3])[[1]][1:10,] | |
## clean up; remove non-ASCII and perform type conversions | |
gdppc$Country <- gsub("Â ", "", gdppc$Country) | |
gdppc$Rank <- iconv(gdppc$Rank, "latin1", "ASCII", sub="") | |
gdppc$Country <- iconv(gdppc$Country, "latin1", "ASCII", sub="") | |
gdppc$`US$` <- as.integer(sub(",", "", gdppc$`US$`)) | |
## flag images (yes, this processing could be done neater, I'm sure) | |
## get the 200px versions | |
flags_img <- html_nodes(html_nodes(html, "table")[3][[1]], "img")[1:10] | |
flags_url <- paste0('http://', sub('[0-9]*px', '200px', sub('\\".*$', '', sub('^.*src=\\"//', '', flags_img)))) | |
flags_name <- sub('.*(Flag_of)', '\\1', flags_url) | |
if(!dir.exists("flags")) dir.create("flags") | |
for(flag in seq_along(flags_url)) { | |
switch(Sys.info()[['sysname']], | |
Windows= {download.file(flags_url[flag], destfile=file.path("flags", paste0(flag,"_", flags_name[flag])), method="auto", mode="wb")}, | |
Linux = {download.file(flags_url[flag], destfile=file.path("flags", paste0(flag,"_", flags_name[flag])))}, | |
Darwin = {print("Not tested on Mac. Use one of the above and find out?")}) | |
} | |
## ########## | |
## END | |
## ########## | |
## load the images from filenames | |
pics <- vector(mode="list", length=npoints) | |
image.file <- dir("flags", full.names=TRUE) | |
image.file <- image.file[order(as.integer(sub("_.*","",sub("flags/","",image.file))))] | |
for(i in 1:npoints) { | |
pics[[i]] <- EBImage::readImage(image.file[i]) | |
} | |
names(pics) <- sub(".svg.png","",sub(".*Flag_of_","",image.file)) | |
## create a dummy dataset | |
npoints <- length(flags_name) | |
y <- gdppc$`US$` | |
x <- names(pics) | |
dat <- data.frame(x=factor(x, levels=names(pics)), y=y) | |
## create the graph, as per normal now with @baptiste's adapted grob processing | |
## NB: #85bb65 is the color of money in the USA apparently. | |
gg <- ggplot(dat, aes(x=x, y=y/1e3L, group=1)) | |
gg <- gg + geom_bar(col="black", fill="#85bb65", stat="identity") | |
gg <- gg + scale_x_discrete() | |
gg <- gg + theme_minimal() | |
gg <- gg + scale_fill_discrete(guide=FALSE) | |
gg <- gg + theme(plot.background = element_rect(fill="grey90")) | |
gg <- gg + labs(title="GDP per capita", | |
subtitle="Top 10 countries", | |
x="", y="$US/1000", | |
caption=paste0("Source: ",url)) | |
gg <- gg + theme(axis.text.x = my_axis(pics), ## that's much better | |
axis.text.y = element_text(size=14), | |
axis.title.x = element_blank()) | |
gg |
This method gets around the element_text()
validation and updates the grobs in a way that’s above my pay grade/understanding of ggplot2
internals, and is a much more consistent way to go about it. This also:
- places the factor labels on the graph along with the picture, covering some concerns about people not knowing which maps are for which country,
- leaves room for the
caption
to go back in, which I wanted, - automatically scales the grob better,
- doesn’t involve creating an external
grob
and thus turning off clipping; usingaxis.text.x
is exactly what I was hoping for.
My version worked (sort of) but only because it used options that were bad practice (not doubting that for a moment). I’d like to see this method make it into ggplot2
properly; Baptiste had an open GitHub issue involving it a while ago but it has since been closed, presumably without the feature being incorporated.
I started the previous post by saying how awesome open-source software is (e.g. R
). You know what else is awesome? The #rstats community. Thank you to every one of you.
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.