How can one control the number of axis ticks within `facet_wrap()`?
Asked Answered
X

3

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

Xanthophyll answered 1/12, 2021 at 16:6 Comment(11)
Are you looking for ... + scale_x_continuous(n.breaks = 2)?Acceptant
I would assume they are, although 3 is probably nicest!Commeasure
@Acceptant removes ticks from second graphExtravagancy
Does this answer your question? Change the number of breaks using facet_grid in ggplot2Financial
Does this answer your question? Increase number of axis ticksCommeasure
@Acceptant I think, I am, great suggestion! but neuron is right, this does not work as hoped.Xanthophyll
@Maël this comes very close, but then the ticks in the first group have unnecessarily many decimal points.Xanthophyll
@Commeasure I actually read this before posting this question, unfortunately it did not help. Thanks for the suggestion!Xanthophyll
@Xanthophyll 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 #48068143Extravagancy
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?Extravagancy
@Extravagancy 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.Xanthophyll
O
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

Oscillation answered 1/12, 2021 at 16:37 Comment(5)
+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).Xanthophyll
Thanks. What are the sequences you have that poses problem?Financial
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)Xanthophyll
That's right! I edited to get some "optimal" rounded values. Let me know if it works well for you ;)Financial
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!Xanthophyll
X
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 :-)

Xanthophyll answered 1/12, 2021 at 17:40 Comment(0)
B
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")

Barsac answered 1/12, 2021 at 16:44 Comment(2)
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."Xanthophyll
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.Barsac

© 2022 - 2025 — McMap. All rights reserved.