Search code examples
rggplot2axis-labels

ggplot2 missing labels after custom scaling of axis


I am attempting to apply a custom scaling of my x-axis using ggplot2 and scales::trans_new(). However, when I do some of the axis labels go missing. Can someone help me figure out why?

Setup:

library(tidyverse)

# the data
ds <- tibble(
  myx = c(1, .5, .1, .01, .001, 0),
  myy = 1:6
)

# the custom transformation
forth_root_trans_rev <- scales::trans_new(
  name = "sign_fourth_root_rev",
  transform = function (x) { - abs(x)^(1/4) },
  inverse = function (x) { x^4 }
)

Plot 1:

When I try and plot this the label for x = 0 gets lost.

# plot - missing x-label at `0`
ggplot(ds, aes(x = myx, y = myy)) + 
  geom_line() + 
  geom_point() + 
  scale_x_continuous(
    trans = forth_root_trans_rev,
    breaks = sort(unique(ds$myx)),
  )

missing x-label at <code>0</code>

Plot 2

When I add some space on both sides of the graph, even more x-labels get lost.

# plot - missing x-labels below 0.5
ggplot(ds, aes(x = myx, y = myy)) + 
  geom_line() + 
  geom_point() +
  scale_x_continuous(
    trans = forth_root_trans_rev,
    breaks = sort(unique(ds$myx)),
    expand = expand_scale(mult = c(.1, .6))
  )

missing x-labels below 0.5

I presume this is related to this old issue: https://github.com/tidyverse/ggplot2/issues/980. Nevertheless, I can't figure out how to apply this transformation and retain all x-labels.

Where am I going wrong?


Solution

  • The problem here is due to the combination of two factors:

    1. Your x-axis values (after transformation) fall in the [-1, 0] range, so any expansion (whether additive or multiplicative) will nudge the final range to cover both positive and negative values.

    2. Your custom transformation is not one-to-one in the [<some negative number>, <some positive number>] region.

    How it occurred

    Somewhere deep inside the all code used to build the ggplot object (you can run ggplot2:::ggplot_build.ggplot before printing the plot & step into layout$setup_panel_params(), but I don't recommend this for casual users... the rabbit hole goes really deep down there), x-axis breaks are calculated in the following manner:

    1. Obtain limits for the transformed values (for c(1, .5, .1, .01, .001, 0) in the question, this will be (-1, 0)).
    2. Add expansion to the limits, if applicable (default expansion for a continuous axis is 5% on either side, so the limits become (-1.05, 0.05)).
    3. Apply the inverse transformation on the limits (taking x^4 on the limits yields (1.215506, 0.000006)).
    4. Apply the transformation on both user-inputted breaks & limits (for breaks, c(1, .5, .1, .01, .001, 0) becomes (-1.0000000, ..., 0.0000000), but for limits, (1.215506, 0.000006) now becomes (-1.05, -0.05), which is narrower than (-1.05, 0.05)).
    5. Breaks beyond the limit's range are dropped (since the limits now stop at -0.05, the break at 0 is dropped).

    How to get around this

    You can modify your transformation with the use of sign() to preserve positive / negative values, such that the transformation is one-to-one in the full range, as suggested by Hadley in the discussion on the GH issue you linked. For example:

    # original
    forth_root_trans_rev <- scales::trans_new(
      name = "sign_fourth_root_rev",
      transform = function (x) { - abs(x)^(1/4) },
      inverse = function (x) { x^4 }
    )
    
    # new
    forth_root_trans_rev2 <- scales::trans_new(
      name = "sign_fourth_root_rev",
      transform = function (x) { -sign(x) * abs(x)^(1/4) },
      inverse = function (x) { -sign(x) * abs(x)^4 }
    )
    
    library(dplyr)
    library(tidyr)
    
    # comparison of two transformations
    # y1 shows a one-to-one mapping in either (-Inf, 0] or [0, Inf) but not both;
    # y2 shows a one-to-one mapping in (-Inf, Inf)
    data.frame(x = seq(-1, 1, 0.01)) %>%
      mutate(y1 = x %>% forth_root_trans_rev$transform() %>% forth_root_trans_rev$inverse(),
             y2 = x %>% forth_root_trans_rev2$transform() %>% forth_root_trans_rev2$inverse()) %>%
      gather(trans, y, -x) %>%
      ggplot(aes(x, y, colour = trans)) +
      geom_line() +
      geom_vline(xintercept = 0, linetype = "dashed") +
      facet_wrap(~trans)
    

    comparison plot

    Usage

    p <- ggplot(ds, aes(x = myx, y = myy)) + 
      geom_line() + 
      geom_point() + 
      theme(panel.grid.minor = element_blank())
    
    p + 
      scale_x_continuous(
        trans = forth_root_trans_rev2,
        breaks = sort(unique(ds$myx))
      )
    p + 
      scale_x_continuous(
        trans = forth_root_trans_rev2,
        breaks = sort(unique(ds$myx)),
        expand = expand_scale(mult = c(.1, .6)) # with different expansion factor, if desired
      )
    

    plots