4
votes

annotation_logticks() is a great little function that adds minor tick marks to a log-scale axis , as shown here.

By default, the function draws ticks on the inside of the panel area of the plot. E.g., if on the y-axis, ticks appear on the right side of the axis line; if on the x-axis, ticks appear above the axis line.

example from ggplot2 website

I find this default undesirable for most plotting situations. Until now I have not found a simple way to get these minor ticks to display on the outside of the panel area where regular ggplot2 ticks appear. So for example on the y-axis appearing on the left side of the line instead of the right. Is there a straightforward solution to this?

For plotting situations where interpolating the values between major ticks is important it would be a very good option to have these appear on the left side where the regular ggplot2 labels and ticks normally appear. I haven't found a solution yet that does not involve clipping or abandoning annotation_logticks() altogether, and I haven;t been successful simply using a negative value for the tick mark length.

Here is the code directly from the ggplot2 website example:

a <- ggplot(msleep, aes(bodywt, brainwt)) +
 geom_point(na.rm = TRUE) +
 scale_x_log10(
   breaks = scales::trans_breaks("log10", function(x) 10^x),
   labels = scales::trans_format("log10", scales::math_format(10^.x))
 ) +
 scale_y_log10(
   breaks = scales::trans_breaks("log10", function(x) 10^x),
   labels = scales::trans_format("log10", scales::math_format(10^.x))
 ) +
 theme_bw()

a + annotation_logticks()

I am looking for a result that simply flips the ticks to the other side of the axis line.

3

3 Answers

4
votes

As far as I can tell, yes but it's a bit gross. TLDR::

b <- a + 
  # reverse ticks
  annotation_logticks(short=unit(-0.1, "cm"), mid=unit(-0.2, "cm"), long=unit(-0.3,"cm")) +
  # remove clipping
  coord_cartesian(clip="off") +
  # add space between ticks and labels
  theme(axis.text.x=element_text(margin = margin(t = 10)), axis.text.y=element_text(margin = margin(r = 10)))

# get the limits of the panel in data coordinates
bb <- ggplot_build(b)$layout$panel_params[[1]]
# draw a white rectangle to cover up the additional tick marks, gross
# the coordinates are actually in powers of 10 of the data
b +
  annotation_custom(
    grob=rectGrob(gp=gpar(col=NA)),
    # the min values are -100 ie "something very large and negative"
    # the max values are the bottom-left corner of the plot plus a tiny
    #  fudge factor to cover up the stubs of the ticks (gross)
    xmin=-100, xmax=min(ab$x.range) + 0.01,
    ymax=min(ab$y.range) + 0.01, ymin=-100
  )

yields

enter image description here

You have to fudge the 0.01s and -100s in the annotation_custom.


If you look at annotation_logticks in the console, it has

layer(data = dumy_data(), # more stuff,
      geo = GeomLogticks, # lots more stuff

ggplot2:::GeomLogticks (it is not an exported function hence the triple ::: to inspect it) shows an object with various functions to draw the ticks. Some trial and error shows that ggplot2:::GeomLogticks$draw_panel appears to be the function that does all the work. It looks like this function makes a dataframe with coordinates of the xstart, xend, ystart, yend of each individual tick (!).

e.g. a snippet from the body of that function

    if (grepl("l", sides)) {
        ticks$y_l <- with(data, segmentsGrob(y0 = unit(yticks$y, 
            "native"), y1 = unit(yticks$y, "native"), x0 = unit(yticks$start, 
            "cm"), x1 = unit(yticks$end, "cm"), gp = gpar(col = alpha(colour, 
            alpha), lty = linetype, lwd = size * .pt)))
    }

From this, it seems we can just provide negative lengths to annotation_logticks

a2 <- a + annotation_logticks(short=unit(-0.1, "cm"), mid=unit(-0.2, "cm"), long=unit(-0.3,"cm"))
a2

enter image description here

This doesn't really work, you can sort of see the stubs of all the tick marks if you squint hard. It looks like the tick marks are drawing correctly, but they are being clipped by the panel area. (If you temporarily turn off the panel border a2 + theme(panel.border=element_rect(fill=NA, color=NA)) you can see this is the case).

To turn off clipping you can do + coord_cartesian(clip='off')

a3 <- a2 + coord_cartesian(clip='off')
a3

enter image description here

As mentioned in ?coord_cartesian for the clip argument, this can lead to "unexpected results" - the ticks are defined for a greater region than the data (I think they do full powers of 10, so if the smallest data point is half-way through a power of 10 it still calculates all the way down to the next lower power) so extend beyond the lower left corner.

A gross way to work around this is to draw a rectangle over the bottom-left part of the graph (outside the panel/in the margin) to cover up the tick marks. annotation_custom does this but requires coordinates in data coordinates. When using the log scale like this, the coordinates are in powers of 10, so e.g. the "10^-4" is coordinate -4.

So to draw the rectangle to cover up the extra marks, we need to have lower-left corner essentially at negative infinity (bottom left corner of the graph), and the upper-right corner at the minimum values on the X/Y axis.

In annotation_custom we want a rectGrob to draw a rectangle. It has a white background by default but a black border which we disable using gpar(col=NA). We set the xmin and ymin coordinates to -100 (essentially something very large and negative; -Inf means "the lowest axis value"). We can set the xmax and ymax coordinates to the lowest axis values, which -Inf will do for us.

a3 +
   annotation_custom(
    grob=rectGrob(gp=gpar(col=NA)),
    xmin=-100, xmax=-Inf,
    ymax=-Inf, ymin=-100
  )

For me this doesn't quite cover up the extra tick marks, leaving annoying little dots.

enter image description here

Really I want the xmax and ymax to be -Inf + a little bit to cover up the entirety of the tick marks. But to do this we can't use -Inf as a shorthand for "lower limit of the axis" any more, we have to access them explicitly using

bb <- ggplot_build(b)$layout$panel_params[[1]]
# bb$x.range, bb$y.range

Then we modify the above

a4 <- a3 +
   annotation_custom(
    grob=rectGrob(gp=gpar(col=NA)),
    xmin=-100, xmax=min(ab$x.range) + 0.01,
    ymax=min(ab$y.range) + 0.01, ymin=-100
  )
a4

(I found the 0.01 by trial and error, that's not great).

This is now fine, except that the X and Y axis labels run into the ticks. To add spacing between the labels and tick marks use

a5 <- a4 + theme(axis.text.x=element_text(margin = margin(t = 10)),
      axis.text.y=element_text(margin = margin(r = 10)))
a5

where these are margins in pixels (you can change the units, see ?margin)

yielding the picture you saw first. You could make it a function perhaps, allowing the user to modify the various fudge factors (the -100, + 0.01, and margin of 10 pixels).

3
votes

I found a tidy way to take care of this, though this solution does not have the same tick length as with annotation_logticks()

logticks <- function(datavar,type) {

  minimum <- 1/10^abs(floor(log10(min(datavar, na.rm=TRUE))))
  maximum <- 1*10^abs(floor(log10(max(datavar, na.rm=TRUE)))+1)
  multiple <- floor(log10(maximum/minimum))

  yourtickvector <- c()

  if (type=="breaks") {

    yourtickvector <- c(minimum)

    for (x in seq(0,multiple)) {

      andadd <- seq(minimum*10^x,minimum*10^(x+1),minimum*10^x)[-1]

      yourtickvector <- c(yourtickvector,andadd)

    }

  } else if (type=="labels") {

    for (x in seq(0,multiple)) {

      andadd <- c(minimum*10^x,rep("",8))

      yourtickvector <- c(yourtickvector,andadd)

    }

    yourtickvector <- c(yourtickvector,minimum*10^multiple)

  }

  return(yourtickvector)

}

# only changed the breaks / label fields below to call the above function

a <- ggplot(msleep, aes(bodywt, brainwt)) +
  geom_point(na.rm = TRUE) +
  scale_x_log10(
    breaks = logticks(msleep$bodywt,"breaks"),
    labels = logticks(msleep$bodywt,"labels")
  ) +
  scale_y_log10(
    breaks = logticks(msleep$brainwt,"breaks"),
    labels = logticks(msleep$brainwt,"labels")
  ) +
  theme_bw()

a

enter image description here

0
votes

I realized if you want the coord_cartesian to work you need to set your "breaks", "limits" and "labels" in the scale command first:

scale_y_log10(breaks = c(0.1, 1, 10, 100, 1000, 10000,100000),limits = c(0.1,100000),labels=c(0.1, 1, 10, 100, 1000, 10000, "100000")) +
  annotation_logticks(sides = "l", outside = T, short = unit(0.05, "cm"),mid = unit(0.05, "cm"),long = unit(0.2, "cm"))+
  coord_cartesian(clip = "off")

ticks outside