How to create base R plot 'type = b' equivalent in ggplot2?

前端 未结 4 2072
日久生厌
日久生厌 2020-12-15 19:40

Base plot() functionality allows one to set type=\'b\' and get a combined line and point plot in which the points are offset from the line segments

4条回答
  •  攒了一身酷
    2020-12-15 20:08

    I'm sorry for answering twice, but this seems sufficiently different to merit a different answer.

    I've given this question some more thought and I'll concede that a geometric approach is indeed the better approach over the point-over-point approach. However, the geometric approach comes with its own set of problems, namely that any attempt at pre-computing coordinates before draw-time is going to give you some skew in one way or another (see a follow up question from @Tjebo).

    It is next to impossible to know the aspect ratio or exact sizes of the plot a priori, except by setting an aspect ratio manually or using the space argument of facet_grid(). Because this is impossible, any precomputed set of coordinates is going to be inadequate if the plot is resized.

    I've shamelessly stolen some good ideas from other people, so thanks to @Tjebo and @moody_mudskipper for the maths and credit to ggplot guru thomasp85 and the ggforce package for the calculating at drawtime inspiration.

    On with it; first we'll define our ggproto as before, now making a custom grob class for our path. An important detail is that we convert our xy coordinates to formal units.

    GeomPointPath <- ggproto(
      "GeomPointPath", GeomPoint,
      draw_panel = function(data, panel_params, coord, na.rm = FALSE){
    
        # Default geom point behaviour
        if (is.character(data$shape)) {
          data$shape <- translate_shape_string(data$shape)
        }
        coords <- coord$transform(data, panel_params)
        my_points <- pointsGrob(
          coords$x, 
          coords$y, 
          pch = coords$shape, 
          gp = gpar(col = alpha(coords$colour, coords$alpha), 
                    fill = alpha(coords$fill, coords$alpha), 
                    fontsize = coords$size * .pt + coords$stroke * .stroke/2, 
                    lwd = coords$stroke * .stroke/2))
    
        # New behaviour
        ## Convert x and y to units
        x <- unit(coords$x, "npc")
        y <- unit(coords$y, "npc")
    
        ## Make custom grob class
        my_path <- grob(
          x = x,
          y = y,
          mult = (coords$size * .pt + coords$stroke * .stroke/2) * coords$mult,
          name = "pointpath",
          gp = grid::gpar(
            col = alpha(coords$colour, coords$alpha),
            fill = alpha(coords$colour, coords$alpha),
            lwd = (coords$linesize * .pt),
            lty = coords$linetype,
            lineend = "butt",
            linejoin = "round", linemitre = 10
          ),
          vp = NULL,
          ### Now this is the important bit:
          cl = 'pointpath'
        )
    
        ## Combine grobs
        ggplot2:::ggname(
          "geom_pointpath",
          grid::grobTree(my_path, my_points) 
        )
      },
      # Adding some defaults for lines and mult
      default_aes = aes(
        shape = 19, colour = "black", size = 1.5, fill = NA, alpha = NA, stroke = 0.5,
        linesize = 0.5, linetype = 1, mult = 0.5,
      )
    )
    

    Through the magic of object oriented programming, we can now write a new method for our new grob class. While that may be uninteresting in and of itself, it gets particularly interesting if we write this method for makeContent, which is called every time a grob is drawn. So, let's write a method that invokes the mathematical operations on the exact coordinates the graphics device is going to use:

    # Make hook for drawing
    makeContent.pointpath <- function(x){
      # Convert npcs to absolute units
      x_new <- convertX(x$x, "mm", TRUE)
      y_new <- convertY(x$y, "mm", TRUE)
    
      # Do trigonometry stuff
      hyp <- sqrt(diff(x_new)^2 + diff(y_new)^2)
      sin_plot <- diff(y_new) / hyp 
      cos_plot <- diff(x_new) / hyp
    
      diff_x0_seg <- head(x$mult, -1) * cos_plot
      diff_x1_seg <- (hyp - head(x$mult, -1)) * cos_plot
      diff_y0_seg <- head(x$mult, -1) * sin_plot
      diff_y1_seg <- (hyp - head(x$mult, -1)) * sin_plot
    
      x0 = head(x_new, -1) + diff_x0_seg
      x1 = head(x_new, -1) + diff_x1_seg
      y0 = head(y_new, -1) + diff_y0_seg
      y1 = head(y_new, -1) + diff_y1_seg
      keep <- unclass(x0) < unclass(x1)
    
      # Remove old xy coordinates
      x$x <- NULL
      x$y <- NULL
    
      # Supply new xy coordinates
      x$x0 <- unit(x0, "mm")[keep]
      x$x1 <- unit(x1, "mm")[keep]
      x$y0 <- unit(y0, "mm")[keep]
      x$y1 <- unit(y1, "mm")[keep]
    
      # Set to segments class
      class(x)[1] <- 'segments'
      x
    }
    

    Now all we need is a layer wrapper like before, which does nothing special:

    geom_pointpath <- function(mapping = NULL, data = NULL, stat = "identity",
                               position = "identity", ..., na.rm = FALSE, show.legend = NA,
                               inherit.aes = TRUE)
    {
      layer(data = data, mapping = mapping, stat = stat, geom = GeomPointPath,
            position = position, show.legend = show.legend, inherit.aes = inherit.aes,
            params = list(na.rm = na.rm, ...))
    }
    

    The demonstration:

    g <- ggplot(pressure, aes(temperature, pressure)) +
      # Ribbon for showing no point-over-point background artefacts
      geom_ribbon(aes(ymin = pressure - 50, ymax = pressure + 50), alpha = 0.2) +
      geom_pointpath()
    

    And this should be stable for any resized aspect ratio. You can supply aes(mult = ...) or just mult = ... to control the size of the gaps between segments. By default it is proportional to the point sizes, so varying the point size while keeping the gap contant is a challenge. Segments that are shorter than two times the gap are deleted.

提交回复
热议问题