简体   繁体   English

如何在ggplot2中创建基本R图'type = b'等价物?

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

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 Base plot()功能允许设置type='b'并获得组合线和点图,其中点与线段偏移

plot(pressure, type = 'b', pch = 19)

在此处输入图片说明

I can easily create a ggplot with lines and points as follows.我可以轻松地创建一个带有线和点的 ggplot,如下所示。

ggplot(pressure, aes(temperature, pressure)) + 
  geom_line() + 
  geom_point()

在此处输入图片说明

The lines, however, run right up to the points.然而,这些线一直延伸到这些点。 I can envision a way that I might hack together something like type='b' functionality using other geoms (eg geom_segment() ?), but I am wondering if there is a more direct way to accomplish this with geom_line() and geom_point() .我可以设想一种方法,我可以使用其他 geoms(例如geom_segment() ?)将type='b'功能组合在一起,但我想知道是否有更直接的方法来使用geom_line()geom_point()

A slightly hacky way of doing this is to overplot a small black point on a larger white point:这样做的一个有点hacky的方法是在更大的白点上过度绘制一个小黑点:

ggplot(pressure, aes(temperature, pressure)) + 
  geom_line() +
  geom_point(size=5, colour="white") + 
  geom_point(size=2) + 
  theme_classic() +
  theme(panel.background = element_rect(colour = "black"))

In addition, following Control point border thickness in ggplot , in version 2.0.0 of ggplot2 it's possible to use the stroke argument of geom_point to control the border thickness, so the two geom_point s can be replaced by just (eg) geom_point(size=2, shape=21, fill="black", colour="white", stroke=3) , eliminating the need to overlay the points.此外,在 ggplot 中的控制点边框厚度之后,在ggplot2的 2.0.0 版本中,可以使用geom_pointstroke参数来控制边框厚度,因此两个geom_point可以替换为(例如) geom_point(size=2, shape=21, fill="black", colour="white", stroke=3) ,无需叠加点。

在此处输入图片说明

One option which is less hacky than manually matching the stroke color with the panel background is to get the panel background beforehand, either from theme_get for the default theme, or with a specific theme that you'll be using.一种比手动将笔触颜色与面板背景匹配更简单的选项是预先获取面板背景,从theme_get获取默认主题,或者使用您将使用的特定主题。 Using a stroked shape like 21 lets you make the inner circle black and the stroke the same color as the background.使用像21这样的描边形状可以使内圈变黑并使描边与背景颜色相同。

library(ggplot2)

bgnd <- theme_get()$panel.background$fill

ggplot(pressure, aes(x = temperature, y = pressure)) + 
  geom_line() + 
  geom_point(shape = 21, fill = "black", size = 2, stroke = 1, color = bgnd)

A couple SO questions ( here's one ) deal with the math behind shortening segments between points.几个 SO 问题(这里是一个)处理缩短点之间的段背后的数学。 It's simple but tedious geometry.这是简单但乏味的几何。 But in the time since this question was first posted, the lemon package has come out, which has a geom to do this.但是自从这个问题第一次发布以来, lemon已经出来了,它有一个geom来做这个。 It's got arguments for how to calculate the shortening, which probably require just some simple tweaking.它有关于如何计算缩短的参数,这可能只需要一些简单的调整。

library(lemon)

ggplot(pressure, aes(x = temperature, y = pressure)) +
  geom_pointline()

Ok I have an implementation of a geom, that does not rely on hardcoding and should not have wierd offsets.好的,我有一个 geom 的实现,它不依赖于硬编码,也不应该有奇怪的偏移量。 It's essentialy a geom_point() implementation, that draws a path* between points, draws a larger background point with colours set to the panel background and then the normal points.它本质上是一个geom_point()实现,它在点之间绘制路径*,绘制一个更大的背景点,颜色设置为面板背景,然后是法线点。

*note that path's behaviour is not to connect points along the x-axis, but along row-order in the data.frame that is given to ggplot. *请注意,路径的行为不是沿 x 轴连接点,而是沿提供给 ggplot 的data.frame中的行顺序连接。 You can sort your data beforehand if you want geom_line() behaviour.如果您想要geom_line()行为,您可以事先对数据进行排序。

The main problem for me was to get the inner workings of the geom drawing code to retrieve the theme of the current plot to extract the background colour of the panel.对我来说,主要问题是获取 geom 绘图代码的内部工作原理,以检索当前绘图的主题以提取面板的背景颜色。 Due to this, I'm very unsure how stable this would be (and would welcome any tips), but at least it works.因此,我非常不确定这会有多稳定(并欢迎任何提示),但至少它有效。

EDIT: should be more stable now编辑:现在应该更稳定

Let's get to the, admittedly lengthy, ggproto object code:让我们进入公认冗长的ggproto目标代码:

GeomPointPath <- ggproto(
  "GeomPointPath", GeomPoint,
  draw_panel = function(self, data, panel_params, coord, na.rm = FALSE)
  {

    # bgcol <- sys.frame(4)$theme$panel.background$fill
    # if (is.null(bgcol)) {
    #   bgcol <- theme_get()$panel.background$fill
    # }

    # EDIT: More robust bgcol finding -----------
    # Find theme, approach as in https://github.com/tidyverse/ggplot2/issues/3116
    theme <- NULL
    for(i in 1:20) {
      env <- parent.frame(i)
      if("theme" %in% names(env)) {
        theme <- env$theme
        break
      }
    }
    if (is.null(theme)) {
      theme <- theme_get()
    }

    # Lookup likely background fills
    bgcol <- theme$panel.background$fill
    if (is.null(bgcol)) {
      bgcol <- theme$plot.background$fill
    }
    if (is.null(bgcol)) {
      bgcol <- theme$rect$fill
    }
    if (is.null(bgcol)) {
      # Default to white if no fill can be found
      bgcol <- "white"
    }
    # END EDIT ------------------

    if (is.character(data$shape)) {
      data$shape <- ggplot2:::translate_shape_string(data$shape)
    }

    coords <- coord$transform(data, panel_params)

    # Draw background points
    bgpoints <- grid::pointsGrob(
      coords$x, coords$y, pch = coords$shape,
      gp = grid::gpar(
        col = alpha(bgcol, NA), 
        fill = alpha(bgcol, NA),
        fontsize = (coords$size * .pt + coords$stroke * .stroke/2) * coords$mult,
        lwd = coords$stroke * .stroke/2
      )
    )

    # Draw actual points
    mypoints <- grid::pointsGrob(
      coords$x, coords$y, pch = coords$shape, 
      gp = grid::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
      )
    )

    # Draw line
    myline <- grid::polylineGrob(
      coords$x, coords$y, 
      id = match(coords$group, unique(coords$group)),
      default.units = "native",
      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
      )
    )

    # Place graphical objects in a tree
    ggplot2:::ggname(
      "geom_pointpath",
      grid::grobTree(myline, bgpoints, mypoints) 
    )
  },
  # Set some defaults, assures that aesthetic mappings can be made
  default_aes = aes(
    shape = 19, colour = "black", size = 1.5, fill = NA, alpha = NA, stroke = 0.5,
    linesize = 0.5, linetype = 1, mult = 3,
  )
)

Observant people may have noticed the line bgcol <- sys.frame(4)$theme$panel.background$fill .细心的人可能已经注意到bgcol <- sys.frame(4)$theme$panel.background$fill I could not find another way to access the current plot's theme, without having to adjust at least several other functions to pass the theme as an argument.我找不到另一种方法来访问当前情节的主题,而不必调整至少其他几个函数来将主题作为参数传递。 In my version of ggplot (3.1.0), the 4th sys.frame() is the environment of the ggplot2:::ggplot_gtable.ggplot_built call wherein the geom drawing code is evaluated.在我的 ggplot (3.1.0) 版本中,第 4 个sys.frame()ggplot2:::ggplot_gtable.ggplot_built调用的环境,其中评估了ggplot2:::ggplot_gtable.ggplot_built绘图代码。 It's quite easy to imagine that this function can be updated in the future -which can change the scoping- hence the stability warning.很容易想象这个功能可以在未来更新 - 这可以改变范围 - 因此稳定性警告。 As a backup, it defaults to the global theme settings when it can't find the current theme.作为备份,当找不到当前主题时,它默认为全局主题设置。

EDIT: should now be more stable编辑:现在应该更稳定

Onwards to the layer wrapper which is pretty much self-explanatory:继续层包装,这几乎是不言自明的:

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, ...))
}

Adding it to a ggplot should be a familiar thing.将它添加到 ggplot 应该是一件熟悉的事情。 Just setting the theme to the default theme_gray() to test that it indeed takes the current plot's theme.只需将主题设置为默认的theme_gray()即可测试它是否确实采用了当前情节的主题。

theme_set(theme_gray())
g <- ggplot(pressure, aes(temperature, pressure)) +
  geom_pointpath() +
  theme(panel.background = element_rect(fill = "dodgerblue"))

Of course, this method will obscure grid lines with the background points, but that is the tradeoff I was willing to make to prevent wonkyness due to line path shortening.当然,这种方法会用背景点模糊网格线,但这是我愿意为防止由于线路径缩短而导致不稳定的权衡。 Line sizes, line types, and the relative size of the background points can be set with aes(linesize = ..., linetype = ..., mult = ...) or per the ... argument in geom_pointpath() .可以使用aes(linesize = ..., linetype = ..., mult = ...)或根据geom_pointpath()...参数设置线条大小、线条类型和背景点的相对大小。 It inherits the other aesthetics from GeomPoint .它继承了GeomPoint的其他美学。

在此处输入图片说明

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).但是,几何方法有其自身的一系列问题,即在绘制时间之前预计算坐标的任何尝试都会以一种或另一种方式给您一些偏斜(请参阅@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() .除了通过手动设置纵横比或使用facet_grid()space参数之外,几乎不可能先验地知道图的纵横比或确切大小。 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.我无耻地从其他人那里窃取了一些好主意,所以感谢 @Tjebo 和 @moody_mudskipper 的数学和功劳,感谢 ggplot 大师thomasp85和 ggforce 包在绘制时进行计算灵感。

On with it;继续吧; first we'll define our ggproto as before, now making a custom grob class for our path.首先,我们将像以前一样定义我们的 ggproto,现在为我们的路径创建一个自定义的 grob 类。 An important detail is that we convert our xy coordinates to formal units.一个重要的细节是我们将 xy 坐标转换为正式单位。

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.通过面向对象编程的魔力,我们现在可以为我们的新 grob 类编写一个新方法。 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.虽然这本身可能很无趣,但如果我们为makeContent编写这个方法就会变得特别有趣,每次绘制makeContent时都会调用它。 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.您可以提供aes(mult = ...)或仅mult = ...来控制段之间的间隙大小。 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.短于间隙两倍的段将被删除。

This is now possible with the CRAN package {ggh4x}.这现在可以通过 CRAN 包 {ggh4x} 实现。 Funny fact, the geom for this package saw the light of the day on this SO post :) Thanks teunbrand!有趣的事实是,这个包的 geom 在这篇 SO 帖子上大放异彩:)谢谢 teunbrand!

library(ggh4x)
#> Loading required package: ggplot2
ggplot(pressure, aes(temperature, pressure)) + 
  geom_pointpath()

Created on 2021-11-13 by the reprex package (v2.0.1)reprex 包(v2.0.1) 于 2021 年 11 月 13 日创建

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM