5

I have decided to rephrase this question. (Editing would have taken more time and in my opinion would also not have helped the OP.)

How can one left-adjust (hjust = 0, i.e., in text direction) over facets, when scale = 'free_x'?

I don't really think that left-adjustment of x-labels is a very necessary thing to do (long labels generally being difficult to read, and right-adjusting probably the better choice) - but I find the problem interesting enough.

I tried with empty padding to the maximum character length, but this doesn't result in the same length for all strings. Also, setting axis.text.x = element.text(margin = margin()) doesn't help. Needless to say, hjust = 0 does not help, because it is adjusting within each facet.

library(ggplot2)

diamonds$cut_label <- paste("Super Dee-Duper", as.character(diamonds$cut))

ggplot(data = diamonds, aes(cut_label, carat)) +
  facet_grid(~ cut, scales = "free_x") +
  theme(axis.text.x = element_text(angle = 90))

enter image description here

The red arrows and dashed line indicate how the labels should adjust. hjust = 0 or margins or empty padding do not result in adjustment of those labels over all facets.

Data modification from this famous question

tjebo
  • 21,977
  • 7
  • 58
  • 94

4 Answers4

5

I tried with empty padding to the maximum character length, but this doesn't result in the same length for all strings.

This caught my attention. Actually, it would result in the same length for all strings if you padded the labels with spaces, made them all the same length, and ensured the font family was non-proportionally spaced.

First, pad the labels with spaces such that all labels have the same length. I'm going to ustilise the str_pad function from the stringr package.

library(ggplot2)

data("diamonds")

diamonds$cut_label <- paste("Super Dee-Duper", as.character(diamonds$cut))

library(stringr)
diamonds$cut_label <- str_pad(diamonds$cut_label, side="right",
                              width=max(nchar(diamonds$cut_label)), pad=" ")

Then, you may need to load a non-proportionally-spaced font using the extrafont package.

library(extrafont)
font_import(pattern='consola')  # Or any other of your choice.

Then, run the ggplot command and specify a proportionally spaced font using the family argument.

ggplot(data = diamonds, aes(cut_label, carat)) +
  facet_grid(~cut, scales = "free_x") +
  theme(axis.text.x = element_text(angle = 90, family="Consolas"))

enter image description here

Edward
  • 10,360
  • 2
  • 11
  • 26
  • 1
    as for importing only one font - see e.g. https://stackoverflow.com/questions/36924251/import-font-into-r-using-extrafont-package – tjebo Mar 14 '20 at 13:50
  • 1
    That's what I hope that someone finds such a solution - it still is beyond my ken. I have added a mildly hacky solution, which is not very satisfying because it needs manual adjustments. – tjebo Mar 14 '20 at 14:41
  • I think you mean "monospaced" (aka fixed width), not "proportionally spaced" – dww Mar 22 '20 at 03:00
  • See [the accepted answer](https://stackoverflow.com/a/60815317/7941188) for the grob solution :) – tjebo Mar 24 '20 at 08:21
  • 1
    That looked easier than I thought. I actually spent 2 hours trying to figure it out but had to give up. >.< Interestingly, all answers use different approaches. Hopefully the ggplot2 folks can find a way to make this easier to do. – Edward Mar 24 '20 at 08:44
  • I think it looks easy because it was done by some real master :) To be fair - I don't think this will have high priority on their list – tjebo Mar 24 '20 at 11:05
4

One way, and possibly the most straight forward hack, would be to annotate outside the coordinates.

Disadvantage is that the parameters would need manual adjustments (y coordinate, and plot margin), and I don't see how to automate this.

library(ggplot2)

diamonds$cut_label <- paste("Super Dee-Duper", as.character(diamonds$cut))

ann_x <- data.frame(x = unique(diamonds$cut_label), y = -16, cut = unique(diamonds$cut))

ggplot(data = diamonds, aes(cut_label, carat)) +
  facet_grid(~cut, scales = "free_x") +
  geom_text(data = ann_x, aes(x, y, label = x), angle = 90, hjust = 0) +
  theme(
    axis.text.x = element_blank(),
    plot.margin = margin(t = 0.1, r = 0.1, b = 2.2, l = 0.1, unit = "in")
  ) +
  coord_cartesian(ylim = c(0, 14), clip = "off")

Created on 2020-03-14 by the reprex package (v0.3.0)

tjebo
  • 21,977
  • 7
  • 58
  • 94
3

I'd approach this by making 2 plots, one of the plot area and one of the axis labels, then stick them together with a package like cowplot. You can use some theme settings to disguise the fact that the axis labels are actually made by a geom_text.

The first plot is fairly straightforward. For the second which becomes the axis labels, use dummy data with the same variables and adjust spacing how you want via text size and scale expansion. You'll probably also want to mess with the rel_heights argument in plot_grid to change the ratio of the two charts' heights.

library(ggplot2)
library(cowplot)

p1 <- ggplot(diamonds, aes(x = cut_label, y = carat)) +
  facet_grid(cols = vars(cut), scales = "free_x") +
  theme(axis.text.x = element_blank()) +
  labs(x = NULL)

axis <- ggplot(dplyr::distinct(diamonds, cut_label, cut), aes(x = cut_label, y = 1)) +
  geom_text(aes(label = cut_label), angle = 90, hjust = 0, size = 3.5) +
  facet_grid(cols = vars(cut), scales = "free_x") +
  scale_x_discrete(breaks = NULL) +
  scale_y_continuous(expand = expansion(add = c(0.1, 1)), breaks = NULL) +
  labs(y = NULL) +
  theme(strip.text = element_blank(),
        axis.text.x = element_blank(),
        axis.ticks = element_blank(),
        panel.background = element_blank())

plot_grid(p1, axis, ncol = 1, axis = "lr", align = "v")

camille
  • 16,432
  • 18
  • 38
  • 60
2

We can edit the text grobs after generating the plot, using library(grid).

g <- ggplot(data = diamonds, aes(cut_label, carat)) +
  facet_grid(~cut, scales = "free_x") +
  theme(axis.text.x = element_text(angle = 90, vjust = 0.5))

gt <- cowplot::as_gtable(g)
axis_grobs <- which(grepl("axis-b", gt$layout$name))
labs <- levels(factor(diamonds$cut_label))[order(levels(diamonds$cut))]

for (i in seq_along(axis_grobs)) {
  gt$grobs[axis_grobs[i]][[1]] <-
    textGrob(labs[i], y = unit(0, "npc"), just = "left", rot = 90, gp = gpar(fontsize = 9))
}

grid.draw(gt)

enter image description here

tjebo
  • 21,977
  • 7
  • 58
  • 94
dww
  • 30,425
  • 5
  • 68
  • 111