6

I have a figure created with facet_wrap visualizing the estimated density of many groups. Some of the groups have a much smaller variance than others. This leads to the x axis not being readable for some panels. Minimum reproducable example:

library(tidyverse)
x1 <- rnorm(1e4)
x2 <- rnorm(1e4,mean=2,sd=0.00001)

data.frame(x=c(x1,x2),group=c(rep("1",length(x1)),rep("2",length(x2)))) %>%
  ggplot(.) + geom_density(aes(x=x)) + facet_wrap(~group,scales="free")

enter image description here

The obvious solution to the problem is to increase the figure size, so that everything becomes readable. However, there are too many panels to make this a useful solution. My favourite solution would be to control the number of axis ticks, for example allow for only two ticks on all x-axes. Is there a way to accomplish this?


Edit after suggestions:

Adding + scale_x_continuous(n.breaks = 2) looks like it should exactly do what I want, but it actually does not:

enter image description here

Following the answer in the suggested question Change the number of breaks using facet_grid in ggplot2, I end up with two axis ticks, but undesirably many decimal points:

equal_breaks <- function(n = 3, s = 0.5, ...){
  function(x){
    # rescaling
    d <- s * diff(range(x)) / (1+2*s)
    seq(min(x)+d, max(x)-d, length=n)
  }
}

data.frame(x=c(x1,x2),group=c(rep("1",length(x1)),rep("2",length(x2)))) %>%
  ggplot(.) + geom_density(aes(x=x)) + facet_wrap(~group,scales="free")  + scale_x_continuous(breaks=equal_breaks(n=3, s=0.05), expand = c(0.05, 0))

enter image description here

LuckyPal
  • 345
  • 2
  • 11
  • 2
    Are you looking for `... + scale_x_continuous(n.breaks = 2)`? – nniloc Dec 01 '21 at 16:14
  • I would assume they are, although `3` is probably nicest! – caldwellst Dec 01 '21 at 16:15
  • 1
    @nniloc removes ticks from second graph – neuron Dec 01 '21 at 16:15
  • 1
    Does this answer your question? [Change the number of breaks using facet\_grid in ggplot2](https://stackoverflow.com/questions/28436855/change-the-number-of-breaks-using-facet-grid-in-ggplot2) – Maël Dec 01 '21 at 16:16
  • Does this answer your question? [Increase number of axis ticks](https://stackoverflow.com/questions/11335836/increase-number-of-axis-ticks) – caldwellst Dec 01 '21 at 16:16
  • @nniloc I think, I am, great suggestion! but neuron is right, this does not work as hoped. – LuckyPal Dec 01 '21 at 16:17
  • @Maël this comes very close, but then the ticks in the first group have unnecessarily many decimal points. – LuckyPal Dec 01 '21 at 16:21
  • @caldwellst I actually read this before posting this question, unfortunately it did not help. Thanks for the suggestion! – LuckyPal Dec 01 '21 at 16:22
  • @LuckyPal you might have to share an example with more than just two panels. I did see this post that talked about how facets are not designed to have custom x-axis ticks like you want https://stackoverflow.com/questions/48067643/specifying-different-x-tick-labels-for-two-facet-groups-in-ggplot2 – neuron Dec 01 '21 at 16:28
  • One thing I did notice is that on my side, the x-axis labels on the right plot actually don't cross over like they do for you. That is likely because my plot is larger than yours. Have you tried saving your real facet_wrap plot as a png yet? – neuron Dec 01 '21 at 16:31
  • @neuron thanks a lot once more for your suggestions. Increasing the figure size is not an option, since my original figure has to many panels to make this doable. – LuckyPal Dec 01 '21 at 16:33

3 Answers3

2

You can add if(seq[2]-seq[1] < 10^(-r)) seq else round(seq, r) to the function equal_breaks developed here.

By doing so, you will round your labels on the x-axis only if the difference between them is above a threshold 10^(-r).

equal_breaks <- function(n = 3, s = 0.05, r = 0,...){
  function(x){
    d <- s * diff(range(x)) / (1+2*s)
    seq = seq(min(x)+d, max(x)-d, length=n)
    if(seq[2]-seq[1] < 10^(-r)) seq else round(seq, r)
  }
}

data.frame(x=c(x1,x2),group=c(rep("1",length(x1)),rep("2",length(x2)))) %>%
  ggplot(.) + geom_density(aes(x=x)) + facet_wrap(~group, scales="free") +
  scale_x_continuous(breaks=equal_breaks(n=3, s=0.05, r=0)) 

enter image description here

As you rightfully pointed, this answer gives only two alternatives for the number of digits; so another possibility is to return round(seq, -floor(log10(abs(seq[2]-seq[1])))), which gets the "optimal" number of digits for every facet.

equal_breaks <- function(n = 3, s = 0.1,...){
  function(x){
    d <- s * diff(range(x)) / (1+2*s)
    seq = seq(min(x)+d, max(x)-d, length=n)
    round(seq, -floor(log10(abs(seq[2]-seq[1]))))
  }
}

data.frame(x=c(x1,x2,x3),group=c(rep("1",length(x1)),rep("2",length(x2)),rep("3",length(x3)))) %>%
  ggplot(.) + geom_density(aes(x=x)) + facet_wrap(~group, scales="free") +
  scale_x_continuous(breaks=equal_breaks(n=3, s=0.1)) 

enter image description here

Maël
  • 45,206
  • 3
  • 29
  • 67
  • +1 Great answer, thank you! For the example data set, it works perfectly fine. However, for my actual data it still produces too many decimal points. I don't understand why ... (of course, you cannot tell neither as you don't know my data). – LuckyPal Dec 01 '21 at 16:52
  • Thanks. What are the sequences you have that poses problem? – Maël Dec 01 '21 at 17:12
  • Your answer only defines two possibilities of rounding, i.e. a maximum and a minimum of decimal places. Try it out with an additional group: `x3 <- rnorm(1e4,mean=2,sd=0.01)` – LuckyPal Dec 01 '21 at 17:36
  • That's right! I edited to get some "optimal" rounded values. Let me know if it works well for you ;) – Maël Dec 01 '21 at 22:03
  • 1
    The `floor(log10())` is a clever trick! For my own use, I added `+1` to the rounding, to avoid asymmetric axis ticks (in your figure, you can see that this happens for the second density, but in my actual data there were much more extreme situations). Now it works like a charm! – LuckyPal Dec 02 '21 at 07:35
2

Thanks so much for so many helpful suggestions and great answers! I figured out a solution that works for arbitrarily complex datasets (at least I hope so) by modifying the approach by @Maël and borrowing the great function by RHertel from Count leading zeros between the decimal point and first nonzero digit.

Rounding to the first significant decimal point leads to highly asymmetric ticks in some cases, therefore I rounded to the second significant decimal point.

library(tidyverse)
x1 <- rnorm(1e4)
x2 <- rnorm(1e4,mean=2,sd=0.000001)
x3 <- rnorm(1e4,mean=2,sd=0.01)

zeros_after_period <- function(x) {
  if (isTRUE(all.equal(round(x),x))) return (0) # y would be -Inf for integer values
  y <- log10(abs(x)-floor(abs(x)))   
  ifelse(isTRUE(all.equal(round(y),y)), -y-1, -ceiling(y))} # corrects case ending with ..01

equal_breaks <- function(n,s){
  function(x){
    x=x*10000
    d <- s * diff(range(x)) / (1+2*s)
    seq = seq(min(x)+d, max(x)-d, length=n) / 10000
    round(seq,zeros_after_period(seq[2]-seq[1])+2)
  }
}

data.frame(x=c(x1,x2,x3),group=c(rep("1",length(x1)),rep("2",length(x2)),rep("3",length(x3)))) %>%
  ggplot(.) + geom_density(aes(x=x)) + facet_wrap(~group, scales="free") +
  scale_x_continuous(breaks=equal_breaks(n=2, s=0.1)) 
 

enter image description here

Apologies for answering my own question ... but that would not have been possible without the great help from the community :-)

LuckyPal
  • 345
  • 2
  • 11
1

One option to achieve your desired result would be to use a custom breaks and limits function which builds on scales::breaks_extended to first get pretty breaks for the range and then makes use of seq to get the desired number of breaks. However, depending on the desired number of breaks this simple approach will not ensure that we end up with pretty breaks:

library(ggplot2)

set.seed(123)
x1 <- rnorm(1e4)
x2 <- rnorm(1e4,mean=2,sd=0.00001)

mylimits <- function(x) range(scales::breaks_extended()(x))

mybreaks <- function(n = 3) {
  function(x) {
    breaks <- mylimits(x)
    seq(breaks[1], breaks[2], length.out = n)  
  }
}

d <- data.frame(x=c(x1,x2),group=c(rep("1",length(x1)),rep("2",length(x2))))
ggplot(d) + 
  geom_density(aes(x=x)) + 
  scale_x_continuous(breaks = mybreaks(n = 3), limits = mylimits) +
  facet_wrap(~group,scales="free")

stefan
  • 90,330
  • 6
  • 25
  • 51
  • This is the best suggestion so far! It works really good, but not perfect. For some panels, fewer ticks appear than for other. This is also mentioned in the help for `breaks_extended()`: "You may get slightly more or fewer breaks that requested." – LuckyPal Dec 01 '21 at 16:55
  • That's right. But I make use of `scales::breaks_extended` only to get pretty breaks for the range. Should have mentioned that my answer is not meant as a general approach to set the number of breaks or ticks. Just an approach to achieve your desired result, i.e. having two ticks for each panel. That#s what my approach is doing, it will give you two pretty breaks for the range and if desired for the midpoint. But the idea could probably be extended to deal with more general cases. – stefan Dec 01 '21 at 17:15