1

I trying to use the trick of using two calls to annotate() to get 2-level nested tick labels (here) BUT I want to do this in conjunction with facet_wrap()

df <- tribble(
  ~action, ~gend, ~status, ~cellMean,
  "P",     "M",   "A",     1,
  "P",     "M",   "B",     2,
  "P",     "F",   "A",     1.4,
  "P",     "F",   "B",     2.6,
  "V",     "M",   "A",     2,
  "V",     "M",   "B",     3,
  "V",     "F",   "A",     2.2,
  "V",     "F",   "B",     3.8
) %>%
  mutate(action=factor(action,levels=c("P","V")),
         gend  =factor(gend,  levels=c("F","M")),
         status=factor(status,levels=c("A","B")))

df

# action obscured/overlaid
df %>%
  ggplot(data=., mapping=aes(x=interaction(status,gend), y=cellMean,
                             color=status, shape=gend)) +
  geom_point(size=3.5) +
  theme_light()

single panel overlaid

# action used as facet
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)

using facet_wrap()

# annotate hack on unfaceted
df %>%
  ggplot(data=., mapping=aes(x=interaction(status,gend), y=cellMean,
                             color=status, shape=gend)) +
  geom_point(size=3.5) +
  annotate(geom = "text", x = 1:4, y = .7, label = rep(c("A","B"), times=2) ) +
  annotate(geom = "text", x = c(1.5,3.5), y = .6, label = c("Females","Males")) +
  coord_cartesian(ylim = c(.8, 4), xlim=c(.5,4.5), expand = FALSE, clip = "off") +
  theme_light() +
  theme(plot.margin = unit(c(1, 1, 4, 1), "lines"),
        axis.title.x = element_blank(),
        axis.text.x = element_blank() )

applying annotate on unfaceted

# annotate hack on FACETED fails saying it wants 8 labels
df %>%
  ggplot(data=., mapping=aes(x=interaction(status,gend), y=cellMean,
                             color=status, shape=gend)) +
  geom_point(size=3.5) +
  annotate(geom = "text", x = 1:4, y = .7, label = rep(c("A","B"), times=2) ) +
  annotate(geom = "text", x = c(1.5,3.5), y = .6, label = c("Females","Males")) +
  coord_cartesian(ylim = c(.8, 4), xlim=c(.5,4.5), expand = FALSE, clip = "off") +
  theme_light() +
  theme(plot.margin = unit(c(1, 1, 4, 1), "lines"),
        axis.title.x = element_blank(),
        axis.text.x = element_blank() ) +
  facet_wrap(~action)

Produces: "Error: Aesthetics must be either length 1 or the same as the data (8): label", which seems to want 8 labels across two facets each with 4 labels.

But, attempts to supply 8 only then demand 16.

# annotate hack on FACETED with length 8 vectors fails saying 16
df %>%
  ggplot(data=., mapping=aes(x=interaction(status,gend), y=cellMean,
                             color=status, shape=gend)) +
  geom_point(size=3.5) +
  annotate(geom = "text", x = rep(1:4, times=2), y = .7, label = rep(c("A","B"), times=4) ) +
  annotate(geom = "text", x = c(1.5,3.5), y = .6, label = c("Females","Males")) +
  coord_cartesian(ylim = c(.8, 4), xlim=c(.5,4.5), expand = FALSE, clip = "off") +
  theme_light() +
  theme(plot.margin = unit(c(1, 1, 4, 1), "lines"),
        axis.title.x = element_blank(),
        axis.text.x = element_blank() ) +
  facet_wrap(~action)

Produces: "Error: Aesthetics must be either length 1 or the same as the data (16): label"

Is there a way to use the annotate() trick with facet_wrap()?

I wonder if I need to make 2 plots and put them side by side to simulate facets.

D. Bontempo
  • 176
  • 1
  • 1
  • 9

1 Answers1

6

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)))

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))

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

Disclaimer: I wrote ggh4x.

teunbrand
  • 33,645
  • 4
  • 37
  • 63
  • Excellent! Thanks. I will educate myself a bit about geom_text() vs annotate(), but this works fine. Is there a reference on using justification values outside the range of 0 to 1? – D. Bontempo Mar 30 '22 at 18:20
  • Does the value of -1 for extend turn off extending or something? – D. Bontempo Mar 30 '22 at 18:30
  • 1
    The `extend = -1` prevents a line being drawn between the top and bottom row of axis labels. For justification: it just expresses how much, in text width (hjust) or text height (vjust) units, the text should be moved relative to the anchorpoint of the text. I don't think there is an official reference for this, but I'd be glad to be proven wrong on this. – teunbrand Mar 30 '22 at 18:50
  • 1
    More info on justification [here](https://stackoverflow.com/questions/7263849/what-do-hjust-and-vjust-do-when-making-a-plot-using-ggplot). – teunbrand Mar 30 '22 at 18:55
  • Your function works great. The only extension I could suggest would be for the lower (higher order) row of axis labels to have larger text or bold text. – D. Bontempo Mar 30 '22 at 19:13
  • 1
    Good suggestion, luckily I had thought of this :) You can control the look of the outer text rows with the theme element `ggh4x.axis.nesttext.x = element_text(colour = "blue")` for example. – teunbrand Mar 30 '22 at 19:16
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/243492/discussion-between-d-bontempo-and-teunbrand). – D. Bontempo Mar 31 '22 at 15:15