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 )
However, switching the scales
parameter to "free"
leads to unexpected behavior, where only D and Q are highlighted.
g + facet_wrap( ~V3, scales = "free" )
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.
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:
gtable
(as gt
above), accessing individual grobs that go into the table can be done through $grobs
.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 )
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" )
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With