Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoiding axis tick label collision in faceted ggplots

In multi-panel/faceted plots, I like when it's feasible to squash the panels together with no space between them, e.g. using theme(panel.spacing=grid::unit(0,"lines")) (Edward Tufte says this is good because the inter-panel spaces produce distracting visual effects, as well as wasting data-space, and I've drunk this particular bit of the Kool-Aid.)

The problem is that, depending on the precise range of the values within a row/column of facets, the axis labels for neighboring facets can overlap. For example, in this plot there's a collision between the bottom tick label in the top panel and the top tick label in the middle panel.

dd <- data.frame(x=rep(1:3,3),
                 y=c(0.1,0.2,0.3,
                     0.1,0.4,0.6,
                     1,2,3),
                 f=factor(rep(letters[1:3],each=3)))
library(ggplot2)
ggplot(dd,aes(x,y))+
   facet_grid(f~.,scale="free")+
   geom_point()+
   theme_bw(base_size=24)+
   theme(panel.spacing=grid::unit(0,"lines"))
ggsave("tmp1.png",width=4,height=6)

enter image description here

I want to build a general, convenient solution to this problem - extending the limits of each facet by an appropriate amount (it will be different for each facet, since the ranges are heterogeneous) but suppressing (at least) labels and (possibly) tick marks for the extreme values. I've done this in the past in a super-hacky way by setting a special-purpose breaks function within scale_y_continuous. I may have thought of some other ways to do this (and will post them as an answer if I can get them to work), but I'm looking for (a) reasonably robust general function(s) for specifying labels and/or breaks.

This is not the same as Automate tick max and min in faceted ggplot , which just wants the max/min values by facet.

This is tricky to do in general and might not be completely solvable; I thought about just blanking out the extreme labels, but that would fail if there were only two or three tick labels. There might be a solution with expand_limits(), but that's hard to do across facets ...

like image 516
Ben Bolker Avatar asked Jan 10 '17 17:01

Ben Bolker


1 Answers

EDIT Updated to ggplot2 ver 3.0.0

One possibility might be to modify the build data. It is possible to take the limits of the original plot, and apply a multiplicative expansion factor. The relative positions of the major and minor breaks also need adjustment. Note that the function allows selection of the panels to which the expansion is to apply.

What's the difference between this and using expand within scale_y_continuous (apart from being able to select the panels)? This function leaves the breaks (and the tick marks and grid lines) as they were in the original. It's just that they take up less space. expand, however, will compress the scale but ggplot will add new labels, grid lines and tick marks if it can.

It would not take much to break the function - ggplot has been known to change the names of the elements of the build data in recent versions.

dd <- data.frame(x=rep(1:3,3),
                 y=c(0.1,0.2,0.3,
                     0.1,0.4,0.6,
                     1,2,3),
                 f=factor(rep(letters[1:3],each=3)))
library(ggplot2)


p = ggplot(dd,aes(x,y))+
   facet_grid(f~.,scale="free")+
   geom_point()+
   theme_bw(base_size=24)+
   theme(panel.spacing=grid::unit(0,"lines"))


expand = function(plot, mult = 0.1, applyTo) {
# Get the build data
gd = ggplot_build(plot)

# Get the original limits,
# and calculate the new limits,
# expanded according to the multiplicative factor
limits <- sapply(gd$layout$panel_params, "[[", "y.range")    # Original limits
fac = mult*(limits[2,] - limits[1, ])  
newlimits = rbind(limits[1, ] - fac, limits[2, ] + fac)

# The range for the new limits
range = newlimits[2, ] - newlimits[1, ]

# Calculate the new y.major and y.minor relative positions
# and put them along with the new limits back into the build data

N = dim(gd$layout$panel_params)[1]  # Number of panels

for(i in applyTo) {
y.major = (gd$layout$panel_params[[i]]$y.major_source - newlimits[1, i]) / range[i]
y.minor = (gd$layout$panel_params[[i]]$y.minor_source - newlimits[1, i]) / range[i]

gd$layout$panel_params[[i]]$y.range = newlimits[, i]

gd$layout$panel_params[[i]]$y.major = y.major
gd$layout$panel_params[[i]]$y.minor = y.minor
}

# Get the gtable
ggplot_gtable(gd)
}


# Plot it
grid::grid.draw(expand(p, mult = 0.1, applyTo = 1:2))

Original
enter image description here

Modified
enter image description here

like image 169
Sandy Muspratt Avatar answered Nov 16 '22 15:11

Sandy Muspratt