0

I need to draw line segments across (and on) the x-axis boundary of a ggplot2 figure so that I can make axis breaks. This SO question is similar but does not have an answer for how to implement multiple axis breaks and the top answer is rather cumbersome.

Example Data

set.seed(321)
dat <- data.frame(matrix(ncol = 2, nrow = 18))
colnames(dat)[1:2] <- c("Month", "Value")
dat$Month <- rep(c(1,2,10,11,20,21),3)
dat$Value <- rnorm(18,20,2)

This is the basic figure, note the theme is theme_bw(). I would like to keep this theme so that this figure resembles others. In the similar SO question, the top answer does not use theme_bw().

library(ggplot2)

ggplot(data = dat, aes(x = factor(Month), y = Value)) +
  geom_boxplot() +
  labs(x = "Month") +
  scale_y_continuous(breaks = seq(15,24,1),
                     limits = c(15,24)) +
  theme_bw() +
  theme(panel.grid = element_blank(),
        text = element_text(size = 16),
        axis.text.x = element_text(size = 14, color = "black"),
        axis.text.y = element_text(size = 14, color = "black"))

This is as far as I got because I could not find a way to extend the geom_segment() across the x-axis boundary.

ggplot(data = dat, aes(x = factor(Month), y = Value)) +
  geom_boxplot() +
  labs(x = "Month") +
  geom_segment(aes(x = 2.45, xend = 2.45,
                   y = -Inf, yend = 15)) +
  geom_segment(aes(x = 2.55, xend = 2.55,
                   y = -Inf, yend = 15)) +
  geom_segment(aes(x = 4.45, xend = 4.45,
                   y = -Inf, yend = 15)) +
  geom_segment(aes(x = 4.55, xend = 4.55,
                   y = -Inf, yend = 15)) +
  scale_y_continuous(breaks = seq(15,24,1),
                     limits = c(15,24)) +
  theme_bw() +
  theme(panel.grid = element_blank(),
        text = element_text(size = 16),
        axis.text.x = element_text(size = 14, color = "black"),
        axis.text.y = element_text(size = 14, color = "black"))

The ideal figure would look like the figure below, which I created 'by-hand' in MS Word which I am trying to avoid. enter image description here

It would be great if ggbreak had a way to do this but it currently does not as far as I am aware.

tassones
  • 891
  • 5
  • 18
  • To extend a geometry across axis I think you need to use `coord_cartesian(clip = "off")`. – yuk Aug 15 '22 at 15:34
  • I've tried that, it did not work. – tassones Aug 15 '22 at 15:37
  • You use `y = -Inf`. Have you tried an actual value below the axis? `y = 14`? – yuk Aug 15 '22 at 15:56
  • If you use a value below the axis it's out of the span of the `limit()` making the whole segment disappear. – Hansel Palencia Aug 15 '22 at 16:02
  • How is the answer cumbersome in the linked thread? It's really not much code, actually less than in Allan's answer. The "cumbersomeness" only arises if you're using oblique separators and want them in a specific angle. The given answer also totally works for more than one break. – tjebo Aug 16 '22 at 20:17

2 Answers2

3

You need to turn off the limits off your y scale. Use the ylim argument in coord_cartesian instead, and set clip = 'off'.

To get the look you are going for, it's probably easiest to add the geom layers as an annotation.

Note also that the annotations are drawn underneath the panel border, so you need to turn that off. In the example below I have drawn it back in using annotation_custom so that the subsequent annotations are drawn over it.

library(ggplot2)

ggplot(data = dat, aes(x = factor(Month), y = Value)) +
  geom_boxplot() +
  labs(x = "Month") +
  scale_y_continuous(breaks = seq(15,24,1)) +
  theme_bw() +
  theme(panel.grid = element_blank(),
        text = element_text(size = 16),
        axis.text.x = element_text(size = 14, color = "black"),
        axis.text.y = element_text(size = 14, color = "black"),
        panel.border = element_blank()) +
  coord_cartesian(clip = 'off', ylim = c(15, 24.5)) +
  annotation_custom(grid::rectGrob(gp = grid::gpar(fill = NA))) +
  annotate('rect', xmin = c(2.33, 4.33), xmax = c(2.67, 4.67),
           ymin = 14, ymax = 16, fill = 'white') +
  annotate('segment', x = c(2.33, 2.67, 4.33, 4.67),
           xend = c(2.33, 2.67, 4.33, 4.67), y = 14.2, yend = 15)

enter image description here

Allan Cameron
  • 147,086
  • 7
  • 49
  • 87
  • I really feel that ggh4x is much better for this use case ... – tjebo Aug 16 '22 at 20:22
  • 1
    I do like your solution too @tjebo. I didn't consider ggh4x. I'm not convinced that using `guide_axis_truncated` here is hugely easier, more concise or more flexible than a couple of annotations, but it certainly does the job. Are there benefits I'm not aware of (other than it feeling a bit less hacky?) I think I may have seen a couple of questions now that want either straight or diagonal lines at the breaks, and the "I" or "|" or "/" that currently have to be added as annotations seem like they could be added as a feature request? – Allan Cameron Aug 16 '22 at 20:32
  • Of course, the final result counts. However, I feel that adding extra layers/annotations (like rectangles!) is potentially error-prone - for example it might potentially result in weird graphical effects, e.g., when saving/opening as pdf (or any vectorised device). It shouldn't be a problem, but I like to keep it "clean" - and if we can draw a segmented axis with quite simple code, then I would favour this solution. – tjebo Aug 16 '22 at 20:38
  • As for the separators - I think a feature request might be reasonable, maybe those separators could be created in a way like suggested in my answer in the previous thread (but then I wouldn't really know how to implement this on draw time, like you guys can magically do ) – tjebo Aug 16 '22 at 20:40
  • 1
    @tjebo sure, but in this case we still need to add a text annotation to get the vertical bars anyway, which makes the case a little weaker. I don't think there would be any doubt which was the 'correct' answer if this feature was added to `ggh4x`. I'm not sure how easy it would be to implement though, since the separators would straddle different parts of the gtable. I think it would have to include either turning clipping off, or drawing on both the panel and the axis tick areas, or some other chicanery to achieve. I have no doubt Teun could do it though. – Allan Cameron Aug 16 '22 at 20:45
3

The answer in the linked thread is not cumbersome and it works really well for this problem.

library(ggh4x)
#> Loading required package: ggplot2
x_end <- c(2.45, 4.45)
x_start <- x_end + 0.1

ggplot(data = dat, aes(x = factor(Month), y = Value)) +
  geom_boxplot() +
  labs(x = "Month") +
  theme_classic() +
  guides(x = guide_axis_truncated(
    trunc_lower = c(-Inf, x_start),
    trunc_upper = c(x_end, Inf)
  )) +
  ## add the segments simply with text annotation - as suggested by teunbrand in the comment in the linked thread
  annotate("text", y = -Inf, x = c(x_end, x_start), label = "I") + 
  coord_cartesian(clip = "off")

tjebo
  • 21,977
  • 7
  • 58
  • 94
  • This answer is acceptable however, it does require the use of an additional package (`ggh4x`; I find it preferable to open the least amount of packages as possible) and this answer does not seem to play well with the `theme_bw()` which is what all my other figures use. When I try to use `theme_bw()` with this answer, the line between the axis breaks is visible. For these reasons, I find @AllanCameron answer to be more easily applied to the posed question. – tassones Aug 17 '22 at 13:31