Ggplot2: Coloring Axis Text on a Faceted Plot

ggplot2: Coloring axis text on a faceted plot

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

Color axis text based on variable value in a faceted plot

So I have found a way to fix the axis colors and scale plots using grid. Based on the above reprex:

# Generate a function to get the legend of one of the ggplots
get_legend<-function(myggplot){
tmp <- ggplot_gtable(ggplot_build(myggplot))
leg <- which(sapply(tmp$grobs, function(x) x$name) == "guide-box")
legend <- tmp$grobs[[leg]]
return(legend)
}

# From the full dataset, find the value of the country with the highest percent of any var_name

max <- round(max(tbl$perc), digits = 2)

# create a sequence of length 6 from 0 to the largest perc value
max_seq <- seq(0, max, length = 6)

# initiate empty list
my_list <- list()

# list of countries to loop through
my_sub <- c("A", "B")

Now we loop through each country, saving each country plot to the empty list.

for(i in my_sub){

### Wrangle
tbl_sub <-
tbl %>%
dplyr::mutate(country = as.factor(country),
domain = as.factor(domain)) %>%
dplyr::filter(country == i),
dplyr::mutate(perc = ifelse(is.na(perc), 0, perc))

# Create custom coord_polar arguments
cp <- coord_polar(theta = "x", clip = "off")
cp$is_free <- function() TRUE

p <-
ggplot(dplyr::filter(tbl_sub, country == i),
aes(x = forcats::as_factor(var_name),
y = perc)) +
cp +
geom_bar(stat = "identity", aes(fill = color)) +
facet_grid(. ~ country, scales = "fixed") +
scale_y_continuous(breaks = c(max_seq),
labels = scales::label_percent(),
limits = c(0, max(max_seq))) +
scale_fill_identity(guide = "legend",
name = "Domain",
labels = c(darkolivegreen4 = "domain a",
orange = "domain c",
navy = "domain b" ,
purple = "domain d",
grey = "not applicable")) +
labs(x = "",
y = "") +
theme_bw() +
theme(aspect.ratio = 1,
panel.border = element_blank(),
strip.text = element_text(size = 16),
axis.title = element_text(size = 18),
title = element_text(size = 20),
axis.text.x = element_text(colour = tbl_new$color, face = "bold"),
legend.text = element_text(size = 14))

my_list[[i]] <- p

}

Now we have the plots in a list, we want to play around with the legend and use grid:: and gridExtra to plot everything together.

# pull legend from first ggplot in the list 
legend <- get_legend(my_list[[1]])

# remove legends from all the plots in the list
for(i in 1:length(my_list)){
my_list[[i]] <- my_list[[i]] + theme(legend.position = "none")
}

# plot everything together
p <- grid.arrange(arrangeGrob(
grobs = my_list,
nrow = round(length(my_sub)/2, 0),
left = textGrob("Y axis",
gp = gpar(fontsize = 20),
rot = 90),
bottom = textGrob("X axis",
gp = gpar(fontsize = 20),
vjust = -3),
top = textGrob("Big plot",
gp = gpar(fontsize = 28, vjust = 2))),
legend = legend,
widths = c(9,1,1),
clip = F)

This yields this image:
Sample Image

The plots are scaled to the country with the largest perc value (0 - 11%), and each country has unique greyed-out values depending on whether there is a 0 or NA in the perc column.

I'm sure there are more simplistic solutions, but this is serving me for now!

Colouring ggplot x axis in facets

If you can live with a rather dirty hack, I can share what I do in these cases. Basically I mess around with the underlying grid structure, which is basically a lot of browser and str calls in the beginning :)

ggplot

p <- ggplot() +
geom_bar(data=dt1,aes(x=myx, y=value), stat="identity") +
facet_grid( ~ activity, scales = "free_x",space = "free_x") +
scale_x_discrete(labels=roles)

grid

Now you have to extract the underlying grob object representing the x-axis to be able to change the color.

library(grid)
bp <- ggplotGrob(p)
wh <- which(grepl("axis-b", bp$layout$name)) # get the x-axis grob

bp$grobs[wh] contains now the two x-axis. Now you have to dive even further into the object to change the color.

bp$grobs[wh] <- lapply(bp$grobs[wh], function(gg) {
## we need to extract the right element
## this is not that straight forward, but in principle I used 'str' to scan through
## the objects to find out which element I would need
kids <- gg$children
wh <- which(sapply(kids$axis$grobs, function(.) grepl("axis\\.text", .$name)))
axis.text <- kids$axis$grobs[[wh]]
## Now that we found the right element, we have to replicate the colour and change
## the element corresponding to 'sophie'
axis.text$gp$col <- rep(axis.text$gp$col, length(axis.text$label))
axis.text$gp$col[grepl("sophie", axis.text$label)] <- "red"
## write the changed object back to the respective slot
kids$axis$grobs[[wh]] <- axis.text
gg$children <- kids
gg
})

So, now 'all' we have to do is to plot the grid object:

grid.draw(bp)

Admittedly, that's rather a rough hack, but it delivers what is needed:

Sample Image

Update

This does not work for more recent versions of ggplot2 as the internal structure of the grob changed. Thus, you need a little adaptation to make it work again. In principle the relevant grob slot moved one slot further down and can be now found in .$children[[1]]

bp$grobs[wh] <- lapply(bp$grobs[wh], function(gg) {
## we need to extract the right element
## this is not that straight forward, but in principle I used 'str' to scan through
## the objects to find out which element I would need
kids <- gg$children
wh <- which(sapply(kids$axis$grobs, function(.) grepl("axis\\.text", .$name)))
axis.text <- kids$axis$grobs[[wh]]$children[[1]]
## Now that we found the right element, we have to replicate the colour and change
## the element corresponding to 'sophie'
axis.text$gp$col <- rep(axis.text$gp$col, length(axis.text$label))
axis.text$gp$col[grepl("sophie", axis.text$label)] <- "red"
## write the changed object back to the respective slot
kids$axis$grobs[[wh]]$children[[1]] <- axis.text
gg$children <- kids
gg
})
grid.draw(bp)

ggplot2: Conditional formatting of x axis label in facet_grid

Well, as the warning tells you it is not recommended to choose the axis text colours by using vectorised theme input (although many people try nonetheless). I believe this was also one of the motivations behind the ggtext package, in which you can use markdown to stylise your text. Below, you'll find an example with a standard dataset, I hope it translates well to yours. We just conditionally apply colouring to some of the x-axis categories.

library(ggplot2)
library(ggtext)
#> Warning: package 'ggtext' was built under R version 4.0.3

df <- transform(mtcars, car = rownames(mtcars))[1:10,]

red_cars <- sample(df$car, 5)

df$car <- ifelse(
df$car %in% red_cars,
paste0("<span style='color:#FF0000'>", df$car, "</span>"),
df$car
)

ggplot(df, aes(car, mpg)) +
geom_col() +
theme(axis.text.x = element_markdown(angle = 90))

Sample Image

Created on 2021-02-03 by the reprex package (v1.0.0)

For more examples, see https://github.com/wilkelab/ggtext#markdown-in-theme-elements

R ggplot multilevel x-axis labels in faceted plots

I also very often have trouble getting annotate() to work nicely with facets. I couldn't get it to work, but you could use geom_text() instead. It takes some finnicking around with clipping, x-label formatting and theme settings to get this to work nicely. I went with vjust = 3, y = -Inf instead of hard-coding the y-position, so that people'll have less trouble generalising this to their plots.

df %>%
ggplot(data=., mapping=aes(x=interaction(status,gend), y=cellMean,
color=status, shape=gend)) +
geom_point(size=3.5) +
geom_text(data = data.frame(z = logical(2)),
aes(x = rep(c(1.5, 3.5), 2), y = -Inf,
label = rep(c("Females", "Males"), 2)),
inherit.aes = FALSE, vjust = 3) +
theme_light() +
coord_cartesian(clip = "off") +
facet_wrap(~action) +
scale_x_discrete(labels = ~ substr(.x, 1, nchar(.x) - 2)) +
theme(axis.title.x.bottom = element_text(margin = margin(t = 20)))

Sample Image

An alternative option is to use ggh4x::guide_axis_nested() to display interaction()ed factors. You'd need to recode your M/F levels to read Male/Female to get a similar result as above.

df %>%
ggplot(data=., mapping=aes(x=interaction(status,gend), y=cellMean,
color=status, shape=gend)) +
geom_point(size=3.5) +
theme_light() +
facet_wrap(~action) +
guides(x = ggh4x::guide_axis_nested(delim = ".", extend = -1))

Sample Image

Created on 2022-03-30 by the reprex package (v2.0.1)

Disclaimer: I wrote ggh4x.

Changing colour of strip.text using facet_wrap of 2 variables

I'm experimenting with custom strips over at the github version of ggh4x. The feature isn't on CRAN yet, but you might find it useful. The main idea is that you can give a list of elements to a strip. Blatantly riffing off stefan's example:

library(ggplot2)
library(ggh4x) # devtools::install_github("teunbrand/ggh4x")

ggplot(mtcars, aes(hp, mpg)) +
geom_point() +
facet_wrap2(
cyl~am,
strip = strip_themed(
text_x = elem_list_text(colour = c("royalblue3", "black")),
by_layer_x = TRUE
)
)

Sample Image

Created on 2021-08-04 by the reprex package (v1.0.0)

(obligatory disclaimer: I'm the author ggh4x. If you find any bugs, it's a great time to report them)

ggplot2 Facet_wrap graph with custom x-axis labels?

There are a few ways to do this... but none that are very direct like you are probably expecting. I'll assume that you want to replace the default x axis title with new titles, so we'll go from there. Here's an example from the iris dataset:

library(ggplot2)

p <- ggplot(iris, aes(Sepal.Length, Sepal.Width)) +
geom_point() +
facet_wrap(~Species)

Sample Image

Use Strip Text Placement

One way to create an axis title specific for each is to use the strip text (also called the facet label). The idea is to position the strip text at the bottom of the facet (usually it's at the top by default) and mess with formatting.

ggplot(iris, aes(Sepal.Length, Sepal.Width)) +
geom_point() +
labs(x=NULL) + # remove axis title
facet_wrap(
~Species,
strip.position = "bottom") + # move strip position
theme(
strip.placement = "outside", # format to look like title
strip.background = element_blank()
)

Sample Image

Here we do a few things:

  1. Remove axis title
  2. Move strip text placement to the bottom, and
  3. Format the strip text to look like an axis title by removing the rectangle around and making sure it is placed "outside" the plot area below the axis ticks

Make your Own Labels with Facet Labels

What about doing what we did above... but making your own labels? You can adjust the strip text labels (facet labels) by setting a named vector as.labeller(). Otherwise, it's the same changes as above. Here's an example:

my_strip_labels <- as_labeller(c(
"setosa" = "My Setosa",
"versicolor" = "Your versicolor",
"virginica" = "Some other stuff"
))

ggplot(iris, aes(Sepal.Length, Sepal.Width)) +
geom_point() +
labs(x=NULL) +
facet_wrap(
~Species, labeller = my_strip_labels, # add labels
strip.position = "bottom") +
theme(
strip.placement = "outside",
strip.background = element_blank()
)

Sample Image

Keep Facet Labels

What about if you want to keep your facet labels, and just add an axis title below each facet? Well, perhaps you can do that via annotation_custom() and make some grobs, but I think it might be easier to place those as a text geom. For this to work, the idea is that you add a text geom outside of your plot area and map the label text itself to the facets. You'll need to do this with a separate data frame (to avoid overlabeling), and the data frame needs to contain two columns: one that is labeled the same as the label of your facetting column, and one that is to be used to store our preferred text for the axis title.

Here's something that works:

axis_titles <- data.frame(
Species = c("setosa", "versicolor", "virginica"),
axis_title = c("Setosa's Axis", "Versi's Axis", "Virgin's Axis")
)

p + labs(x=NULL) +
geom_text(
data=axis_titles,
aes(label=axis_title), hjust=0.5,
x=min(iris$Sepal.Length) + diff(range(iris$Sepal.Length))/2,
y=1.7, color='red', fontface='bold'
) +
coord_cartesian(clip="off") +
theme(
plot.margin= margin(b=30)
)

Sample Image

Here we have to do a few things:

  1. Create the data frame to store our axis titles
  2. Remove default axis title
  3. Add a geom_text() linked to the new data frame and modify placement. Note I'm mathematically fixing the position to be "in the middle" of the x axis. I manually placed the y value, but you could use an equation there too if you want.
  4. Turn clip="off". This is important, because with clip="on" it will prevent any geoms from being shown if they are outside the panel area.
  5. Extend the plot margin down a bit so that we can actually see our text.

R ggplot2: change colour of font and background in facet strip?

Another option is using grid's editing functions, provided that we build the gPath of each grob that we want to edit.

Prepare the gPaths:

library(ggplot2)
library(grid)

p <- ggplot(mpg, aes(displ, cty)) + geom_point() + facet_grid(drv ~ cyl)

# Generate the ggplot2 plot grob
g <- grid.force(ggplotGrob(p))
# Get the names of grobs and their gPaths into a data.frame structure
grobs_df <- do.call(cbind.data.frame, grid.ls(g, print = FALSE))
# Build optimal gPaths that will be later used to identify grobs and edit them
grobs_df$gPath_full <- paste(grobs_df$gPath, grobs_df$name, sep = "::")
grobs_df$gPath_full <- gsub(pattern = "layout::",
replacement = "",
x = grobs_df$gPath_full,
fixed = TRUE)

Check out the table grobs_df and get familiar with the naming and paths. For example all strips contain the key word "strip". Their background is identified by the key word "background" and their title text by "titleGrob" & "text". We can then use regular expression to catch them:

# Get the gPaths of the strip background grobs
strip_bg_gpath <- grobs_df$gPath_full[grepl(pattern = ".*strip\\.background.*",
x = grobs_df$gPath_full)]
strip_bg_gpath[1] # example of a gPath for strip background
## [1] "strip-t-1.7-5-7-5::strip.1-1-1-1::strip.background.x..rect.5374"

# Get the gPaths of the strip titles
strip_txt_gpath <- grobs_df$gPath_full[grepl(pattern = "strip.*titleGrob.*text.*",
x = grobs_df$gPath_full)]
strip_txt_gpath[1] # example of a gPath for strip title
## [1] "strip-t-1.7-5-7-5::strip.1-1-1-1::GRID.titleGrob.5368::GRID.text.5364"

Now we can edit the grobs:

# Generate some color
n_cols <- length(strip_bg_gpath)
fills <- rainbow(n_cols)
txt_colors <- gray(0:n_cols/n_cols)

# Edit the grobs
for (i in 1:length(strip_bg_gpath)){
g <- editGrob(grob = g, gPath = strip_bg_gpath[i], gp = gpar(fill = fills[i]))
g <- editGrob(grob = g, gPath = strip_txt_gpath[i], gp = gpar(col = txt_colors[i]))
}

# Draw the edited plot
grid.newpage(); grid.draw(g)
# Save the edited plot
ggsave("edit_strips_bg_txt.png", g)

Sample Image



Related Topics



Leave a reply



Submit