ggplot2 annotation_ticks on the outside of the plot region

梦想的初衷 提交于 2019-12-13 16:15:16

问题


I try to find an elegant way to insert minor ticks on plots created with ggplot2. I found a function which does almost exactly what I want: https://rdrr.io/github/hrbrmstr/ggalt/src/R/annotation_ticks.r

There is only one drawback: the ticks, like in annotation_logticks, are drawn inside the plot region. I need them to be on the outside.

A solution could be to use negative values for the tick-length. When I do so, the ticks disappear. I assume, that this is due to the default clipping action of ggplot2, which supresses plotting outside the plot region (?) (see also log ticks on the outer side of axes (annotation_logticks), where the clipping is turned off which - unfortunately - leads to ticks exceeding the plot-range).

So: is there an option to modify the annotation_ticks - function in order to produce ticks outside of the plot region, only covering the range of the plot? Ideally, this functionality should by incorporated in the annotate_ticks - function (I don't want to save and then re-arrange the plot; I'd rather build my final plot in one step).


回答1:


I've found a sort of satisfactory solution to adapting the annotation_ticks function. If we'd simply copy-paste the code from the link you've posted, we can make the following small adjustment near the end in the GeomTicks ggproto object:

GeomTicks <- ggproto(
  "GeomTicks", Geom,
  # ...
  # all the rest of the code
  # ...
    gTree(children = do.call("gList", ticks), cl = "ticktrimmer") # Change this line
  },
  default_aes = aes(colour = "black", size = 0.5, linetype = 1, alpha = 1)
)

Then we can write a small function that simply clips the ticks that are outside the range that gets triggered just before drawing by hijacking the S3 generic makeContent in the grid package:

library(grid)

makeContent.ticktrimmer <- function(x) {
  # Loop over segment grobs
  x$children <- lapply(x$children, function(m) {
    # convert positions to values
    x0 <- convertX(m$x0, "npc", valueOnly = T)
    x1 <- convertX(m$x1, "npc", valueOnly = T)
    y0 <- convertY(m$y0, "npc", valueOnly = T)
    y1 <- convertY(m$y1, "npc", valueOnly = T)

    # check if values are outside 0-1
    if (length(unique(x0)) == 1) {
      keep <- y0 >= 0 & y0 <= 1 & y1 >= 0 & y1 <= 1
    } else if (length(unique(y0)) == 1) {
      keep <- x0 >= 0 & x0 <= 1 & x1 >= 0 & x1 <= 1
    } else {
      keep <- TRUE
    }

    # Trim the segments
    m$x0 <- m$x0[keep]
    m$y0 <- m$y0[keep]
    m$x1 <- m$x1[keep]
    m$y1 <- m$y1[keep]
    m
  })
  x
}

And now we can plot:

g <- ggplot(iris, aes(Sepal.Width, Sepal.Length)) +
  geom_point(aes(colour = Species)) +
  annotation_ticks(long = -1 * unit(0.3, "cm"),
                   mid = -1 * unit(0.2, "cm"),
                   short = -1 * unit(0.1, "cm")) +
  coord_cartesian(clip = "off")

Besides the first tick on the left being slightly weirdly placed, this seems to work reasonably.

EDIT: Here is a quick refactoring of the code to work with the native minor breaks instead of calculating minor breaks de novo. The user function:

annotation_ticks <- function(sides = "b",
                             scale = "identity",
                             scaled = TRUE,
                             ticklength = unit(0.1, "cm"),
                             colour = "black",
                             size = 0.5,
                             linetype = 1,
                             alpha = 1,
                             color = NULL,
                             ticks_per_base = NULL,
                             ...) {
  if (!is.null(color)) {
    colour <- color
  }

  # check for invalid side
  if (grepl("[^btlr]", sides)) {
    stop(gsub("[btlr]", "", sides), " is not a valid side: b,t,l,r are valid")
  }

  # split sides to character vector
  sides <- strsplit(sides, "")[[1]]

  if (length(sides) != length(scale)) {
    if (length(scale) == 1) {
      scale <- rep(scale, length(sides))
    } else {
      stop("Number of scales does not match the number of sides")
    }
  }

  base <- sapply(scale, function(x) switch(x, "identity" = 10, "log10" = 10, "log" = exp(1)), USE.NAMES = FALSE)

  if (missing(ticks_per_base)) {
    ticks_per_base <- base - 1
  } else {
    if ((length(sides) != length(ticks_per_base))) {
      if (length(ticks_per_base) == 1) {
        ticks_per_base <- rep(ticks_per_base, length(sides))
      } else {
        stop("Number of ticks_per_base does not match the number of sides")
      }
    }
  }

  delog <- scale %in% "identity"

  layer(
    data = data.frame(x = NA),
    mapping = NULL,
    stat = StatIdentity,
    geom = GeomTicks,
    position = PositionIdentity,
    show.legend = FALSE,
    inherit.aes = FALSE,
    params = list(
      base = base,
      sides = sides,
      scaled = scaled,
      ticklength = ticklength,
      colour = colour,
      size = size,
      linetype = linetype,
      alpha = alpha,
      ticks_per_base = ticks_per_base,
      delog = delog,
      ...
    )
  )
}

The ggproto object:

GeomTicks <- ggproto(
  "GeomTicks", Geom,
  extra_params = "",
  handle_na = function(data, params) {
    data
  },

  draw_panel = function(data,
                        panel_scales,
                        coord,
                        base = c(10, 10),
                        sides = c("b", "l"),
                        scaled = TRUE,
                        ticklength = unit(0.1, "cm"),
                        ticks_per_base = base - 1,
                        delog = c(x = TRUE, y = TRUE)) {
    ticks <- list()

    for (s in 1:length(sides)) {
      if (grepl("[b|t]", sides[s])) {

        xticks <- panel_scales$x.minor

        # Make the grobs
        if (grepl("b", sides[s])) {
          ticks$x_b <- with(
            data,
            segmentsGrob(
              x0 = unit(xticks, "npc"),
              x1 = unit(xticks, "npc"),
              y0 = unit(0, "npc"),
              y1 = ticklength,
              gp = gpar(
                col = alpha(colour, alpha),
                lty = linetype,
                lwd = size * .pt
              )
            )
          )
        }
        if (grepl("t", sides[s])) {
          ticks$x_t <- with(
            data,
            segmentsGrob(
              x0 = unit(xticks, "npc"),
              x1 = unit(xticks, "npc"),
              y0 = unit(1, "npc"),
              y1 = unit(1, "npc") - ticklength,
              gp = gpar(
                col = alpha(colour, alpha),
                lty = linetype,
                lwd = size * .pt
              )
            )
          )
        }
      }


      if (grepl("[l|r]", sides[s])) {

        yticks <- panel_scales$y.minor

        # Make the grobs
        if (grepl("l", sides[s])) {
          ticks$y_l <- with(
            data,
            segmentsGrob(
              y0 = unit(yticks, "npc"),
              y1 = unit(yticks, "npc"),
              x0 = unit(0, "npc"),
              x1 = ticklength,
              gp = gpar(
                col = alpha(colour, alpha),
                lty = linetype, lwd = size * .pt
              )
            )
          )
        }
        if (grepl("r", sides[s])) {
          ticks$y_r <- with(
            data,
            segmentsGrob(
              y0 = unit(yticks, "npc"),
              y1 = unit(yticks, "npc"),
              x0 = unit(1, "npc"),
              x1 = unit(1, "npc") - ticklength,
              gp = gpar(
                col = alpha(colour, alpha),
                lty = linetype,
                lwd = size * .pt
              )
            )
          )
        }
      }
    }
    gTree(children = do.call("gList", ticks))
  },
  default_aes = aes(colour = "black", size = 0.5, linetype = 1, alpha = 1)
)

Plotting:

ggplot(iris, aes(Sepal.Width, Sepal.Length)) +
  geom_point(aes(colour = Species)) +
  annotation_ticks(ticklength = -1 * unit(0.1, "cm"),
                   side = "b") +
  coord_cartesian(clip = "off")




回答2:


This is indeed a very elegant solution, however, if you are working with facets the annotation minor ticks will be repeated in every plot. I've changed the original tick sizes to make it more evident, see the example below:

ggplot(iris, aes(Sepal.Width, Sepal.Length)) +
  geom_point(aes(colour = Species)) +
  facet_wrap (~Species, dir='v') +
  annotation_ticks(ticklength = -1 * unit(0.1, "cm"),
                   side = "b") +
  coord_cartesian(clip = "off") +
  theme (axis.ticks.length.x=unit(.5, "cm"))

Is there a way to prevent it from happening?



来源:https://stackoverflow.com/questions/58485334/ggplot2-annotation-ticks-on-the-outside-of-the-plot-region

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!