Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ggplot2: Coloring axis text on a faceted plot

I seem unable to correctly color axis text on a faceted plot when the scales parameter is set to "free". Consider the following dataset:

library( ggplot2 )
X <- data.frame( V1 = LETTERS, V2 = runif( 26 ),
    V3 = rep( c("F1", "F2"), each = 13 ) )

We can plot the data on a single facet, highlighting the letters D, O, T as follows:

v <- ifelse( X$V1 %in% c( "D", "O", "T" ), "red", "black" )
g <- ggplot( X, aes( x = V1, y = V2 ) ) + geom_point() +
    theme( axis.text.x = element_text( color = v ) )

Making the plot faceted using the default scales = "fixed" correctly highlights D, O, T on both facets.

g + facet_wrap( ~V3 )

enter image description here

However, switching the scales parameter to "free" leads to unexpected behavior, where only D and Q are highlighted.

g + facet_wrap( ~V3, scales = "free" )

enter image description here

My question: is this a bug or do I need to somehow modify my definition of v to account for free scales. If it is a bug, does anybody know of a workaround to highlight specific axis text in each (free-scaled) facet?


EDIT: Own answer moved to answers, as suggested by Henrik.

like image 515
Artem Sokolov Avatar asked Aug 23 '17 15:08

Artem Sokolov


2 Answers

After digging through the graphical objects (grobs) associated with the plot, I came across a potential hack to get around the issue. While not as elegant as Z.Lin's solution, I wanted to share it for educational purposes.

We begin by retrieving grobs with

gt <- ggplotGrob( g + facet_wrap( ~V3, scales = "free" ) )
## TableGrob (11 x 11) "layout": 20 grobs
##     z         cells        name                                  grob
## 1   0 ( 1-11, 1-11)  background       rect[plot.background..rect.105]
## 2   1 ( 7- 7, 4- 4)   panel-1-1               gTree[panel-1.gTree.17]
## 3   1 ( 7- 7, 8- 8)   panel-2-1               gTree[panel-2.gTree.30]
## 4   3 ( 5- 5, 4- 4)  axis-t-1-1                        zeroGrob[NULL]
## 5   3 ( 5- 5, 8- 8)  axis-t-2-1                        zeroGrob[NULL]
## 6   3 ( 8- 8, 4- 4)  axis-b-1-1    absoluteGrob[GRID.absoluteGrob.43]
## 7   3 ( 8- 8, 8- 8)  axis-b-2-1    absoluteGrob[GRID.absoluteGrob.50]
## 8   3 ( 7- 7, 7- 7)  axis-l-1-2    absoluteGrob[GRID.absoluteGrob.64]
## 9   3 ( 7- 7, 3- 3)  axis-l-1-1    absoluteGrob[GRID.absoluteGrob.57]
## 10  3 ( 7- 7, 9- 9)  axis-r-1-2                        zeroGrob[NULL]
## 11  3 ( 7- 7, 5- 5)  axis-r-1-1                        zeroGrob[NULL]
## 12  2 ( 6- 6, 4- 4) strip-t-1-1                         gtable[strip]
## 13  2 ( 6- 6, 8- 8) strip-t-2-1                         gtable[strip]
## 14  4 ( 4- 4, 4- 8)      xlab-t                        zeroGrob[NULL]
## 15  5 ( 9- 9, 4- 8)      xlab-b titleGrob[axis.title.x..titleGrob.33]
## 16  6 ( 7- 7, 2- 2)      ylab-l titleGrob[axis.title.y..titleGrob.36]
## 17  7 ( 7- 7,10-10)      ylab-r                        zeroGrob[NULL]
## 18  8 ( 3- 3, 4- 8)    subtitle zeroGrob[plot.subtitle..zeroGrob.102]
## 19  9 ( 2- 2, 4- 8)       title    zeroGrob[plot.title..zeroGrob.101]
## 20 10 (10-10, 4- 8)     caption  zeroGrob[plot.caption..zeroGrob.103]

Grobs are hierarchical objects and the general rules for traversing these structures fall into two categories:

  1. If a grob is of type gtable (as gt above), accessing individual grobs that go into the table can be done through $grobs.
  2. If a grob is NOT of type gtable, its children grobs can be accessed through $children.

Looking at the gtable above, we observe that grobs 6 and 7 correspond to the bottom axes of facets 1 and 2, respectively. Each of these axis grobs is of type absoluteGrob, so using the two rules above, we can examine what they are made up of like this:

gt$grobs[[6]]$children
## (zeroGrob[axis.line.x..zeroGrob.40], gtable[axis]) 
##  and likewise for gt$grobs[[7]]$children

Noting that the second child is a gtable, we can continue descending the hierarchy of grobs until we arrive at gt$grobs[[6]]$children[[2]]$grobs[[2]]$children[[1]], which is a leaf of the grob hierarchy (its $children is NULL) and corresponds to the axis text. Let's examine its graphical parameters, which can be accessed through $gp:

## Double-check that we have the correct text object
gt$grobs[[6]]$children[[2]]$grobs[[2]]$children[[1]]$label
## [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M"

## Display the summary of graphical parameters
str( gt$grobs[[6]]$children[[2]]$grobs[[2]]$children[[1]]$gp )
## List of 5
##  $ fontsize  : num 8.8
##  $ col       : chr [1:26] "black" "black" "black" "red" ...
##  $ fontfamily: chr ""
##  $ lineheight: num 0.9
##  $ font      : Named int 1
##   ..- attr(*, "names")= chr "plain"
##  - attr(*, "class")= chr "gpar"

Note that the col attribute is of length 26 and corresponds exactly to the v variable from the question. If we look at the bottom axis of the second facet (gt$grobs[[7]]$...), we see that the same col value is used there as well, leading to identical axis text coloring in both facets (as suggested in Z.Lin's solution).

Consequently, setting these color setting to only be the corresponding portions of v "by hand" allows us to modify the original plot and achieve the desired result.

gt$grobs[[6]]$children[[2]]$grobs[[2]]$children[[1]]$gp$col <- v[1:13]
gt$grobs[[7]]$children[[2]]$grobs[[2]]$children[[1]]$gp$col <- v[14:26]
grid::grid.draw( gt )

enter image description here

like image 195
Artem Sokolov Avatar answered Oct 21 '22 18:10

Artem Sokolov


I don't think it's a bug. The problem is that v here is basically a string of characters, length 26, which defines colours for the first 26 breaks on the x-axis. When the x-axis has 26 breaks exactly, well & good; when it has less than that (which is the case when you set scales="free"), it simply restarts at the beginning for each axis. Q is red here because it's in the fourth position in the second plot, although the v[4]'s red was meant for D, in the first plot.

Based on what I've tried & read here on SO, one can't map aesthetics into theme(), which controls the appearance of axis text in ggplot.

It's possible to hack a solution by hiding the axis & using geom_text() instead to simulate an axis, since the latter does accept aesthetics mapped from the data. It may not be very elegant, though:

g2 <- ggplot(cbind(X, v), #add v to X
             aes(x = V1, y = V2)) + 
  geom_point() +
  # make space to accommodate the fake axis
  expand_limits(y = -0.05) +
  # create a strip of white background under the fake axis
  geom_rect(ymin = -5, ymax = 0, xmin = 0, xmax = nrow(X) + 1, fill = "white") +
  # fake axis layer, aligned below y = 0
  geom_text(aes(colour = v, label = V1), y = 0, vjust = 1.1) +
  # specify the font colours for fake axis
  scale_colour_manual(values = c("black", "red"), guide = F) +
  # hide the actual x-axis text / ticks
  theme(axis.text.x = element_blank(), axis.ticks.x = element_blank())

g2 + facet_wrap( ~V3, scales = "free" )

facet plot with fake axis

like image 29
Z.Lin Avatar answered Oct 21 '22 19:10

Z.Lin