如何在ggplot中对geom应用裁剪掩膜?

9
我正在尝试将剪切蒙版应用于 ggplot 中的几何对象,以遮蔽部分数据,但保留轴、网格、其他几何对象和图例的可见性。我不想创建特定的绘图,因此不想使用多边形掩盖绘图的某些部分来解决问题。
这是我想要模仿的设计(蒙版,不一定是主题,我知道如何做):

map with a central round clipping mask on some layers

(来源)

另请参阅此示例

有人可能会认为我可以过滤掉未包含在遮罩定义多边形中的数据。但是,虽然对于点可以起作用,并且对于类似于多边形/线的对象也可以起作用,但对于栅格数据则不行(边界将不会完全遵循非垂直或非水平线)。因此,我尝试了以下方法:

library(ggplot2)
library(gridSVG)
library(grImport)

# Create a plot
p <- ggplot(diamonds[1:300,], aes(carat, price)) + geom_point(aes(colour = cut))

# And a clipping mask
pg <- polygonGrob(c(.7, 0, 0, 1, 1),
              c(0, .7, 1, 1, 0))
cp <- clipPath(pg)

我可以使用 gridSVG 包来定义一个裁剪蒙版,但是我在将其应用于 ggplot 对象时遇到了困难,即使在使用 ggplotGrob() 提取 grob(参见此处)之后。我无法将裁剪蒙版应用于 grob:
g <-  ggplotGrob(p) # store the plot as a grob

registerClipPath("mask", cp)
g_clipped <- clipPath(g)

gridsvg(name = "test_c2.svg")
grid.draw(clipPathGrob(g_clipped, cp)$grob)
dev.off()

我的直觉是应该绘制g_clipped,但我无法使用grid.draw()来绘制它,因为它是一个clipPath对象。而且这里写的grid.draw()行显示了未被遮罩的图形。我想我不太明白clipPath对象的功能。
函数grobify()听起来可以帮助找到没有使用gridSVG的替代方法,请参见此处的详细信息,但我并不理解这个相当简洁的文档。
由于我甚至不能将裁剪掩码应用于整个图形,所以我离我的目标还很远。
如果您能帮助我理解如何应用来自gridSVG的裁剪掩码,或者有一个替代方案来将裁剪掩码应用于特定的几何图形,请告诉我。
2个回答

2

由于您从一个ggplot对象开始,因此创建掩码本身作为几何图层可能比转换所有内容为grob并在网格系统中工作更简单。

可以使用来自ggpolypath软件包的geom_polypath()函数。与ggplot2中的标准geom_polygon不同,它能够处理具有孔的多边形(请参见vignette):

# sample data frame for clipping. The first four x & y coordinates are for the outer ends;
# the next four are for the hole in the polygon.
clipping.df <- data.frame(x = c(0, 1.5, 1.5, 0, 0.2, 1, 0.7, 0.3),
                          y = c(0, 0, 3000, 3000, 250, 2000, 2800, 1500),
                          hole = rep(c(FALSE, TRUE), each = 4),
                          group = rep(c("1", "2"), each = 4))

library(ggpolypath)
p +
  geom_polypath(data = clipping.df,
                aes(x = x, y = y, group = group),
                colour = NA, fill = "black", alpha = 0.5,
                inherit.aes = FALSE) +
  scale_x_continuous(expand = c(0, 0)) + # don't show edges beyond the extent
  scale_y_continuous(expand = c(0, 0))   # of the polygon

plot


谢谢这个想法。对于某些情况它可能有效,然而它不能达到我的目标。这种方法的两个主要限制是:(1)它遮盖了网格,(2)它遮盖了geom下面的所有东西,而剪切蒙版方法允许你遮盖一些层,但不包括下面的层。我会继续挖掘的 ;) - Hobo Sheep
也许您可以提供一个示例来展示您期望的输出?至于您的第一个问题,theme()有一个panel.ontop选项,它可以将网格放置在数据层上方。 - Z.Lin
也许这张图片更好地代表了剪切蒙版的兴趣(在灰色区域应用)。我不知道这一点,所以谢谢你。然而,这仍然不完全令人满意,因为网格现在会重叠在所有东西上,而剪切蒙版将允许保持几何图形在网格前面。因此,我真正希望以一种一致和结构化的方式应用剪切蒙版到几何图形上,而不是通过解决特定的一次性绘图来实现。 - Hobo Sheep

2
以下是一种网格解决方案,但非常具有变通性。它展示了如何向ggplot应用一个非矩形剪切区域,以便在您的图中剪切一组点。你在尝试中并没有太远。需要注意以下几点:
  1. 您需要使用grid.force()函数来强制执行ggplotGrob对象,以便grid可以查看这些grobs。
  2. 不要将ggplot grob定义为剪切路径 - 剪切路径是多边形。
  3. 剪切路径应用于ggplot中绘图面板内的点grobs。这意味着绘图面板中的其他对象(面板背景和网格线)不会被剪切。只有数据点被剪切。
我已经向图中添加了一条蓝色线,以表明该线也不需要被剪切; 但如果需要,也可以进行剪切。
代码中还有注释的行,取消注释后可以绘制剪切区域,并将网格线和点移动到最前面(即在较暗的灰色剪切区域的前面)。
library(ggplot2)
library(gridSVG)
library(grid)

# Open the graphics device
gridsvg(name = "test.svg")

# Create a plot
p <- ggplot(diamonds[1:300, ], aes(carat, price)) + 
       geom_point(aes(colour = cut)) +
       geom_line(data = data.frame(x = c(.3, .9), y = c(500, 2500)), aes(x,y), col = "skyblue", size = 2)
g <- ggplotGrob(p) # Store the plot as a grob


g = grid.force(g)  # So that grid sees all grobs
grid.draw(g)       # Draw the plot

# Define the clipping path
pg <- polygonGrob(c(.7, 0, 0, 1, 1),
                  c(0, .7, 1, 1, 0))
# The clipping path can be nearly any shape you desire. 
# Try this for a circular region
# pg = circleGrob(x = .5, y = .6, r = .5)
cp <- clipPath(pg)

# Add the clipping path to the points grob.
# That is, only the points inside the polygon will be visible,
# but the background and grid lines will not be clipped. 
# Nor will the blue line be clipped.
# grid.ls(g)     # names of the grobs
seekViewport(grep("panel.[0-9]", grid.ls(g)$name, value = TRUE))
grid.clipPath("points", cp, grep = TRUE)   

# To clip the blue line, uncomment the next line
# grid.clipPath("GRID.polyline", cp, grep = TRUE)       

# To show the clipping region,    
# uncomment the next two lines.
# showcp = editGrob(pg, gp = gpar(fill = rgb(0, 0, 0, 0.05), col = "transparent"))
# grid.draw(showcp)

# And to move the grid lines, remaining data points, and blue line in front of the clipping region,
# uncomment the next five lines
# panel = grid.get("panel", grep = TRUE)   # Get the panel, and remove the background grob
# panel = removeGrob(panel, "background", grep = TRUE)

# grid.remove("points", grep = TRUE)     # Remove points and grid lines from the rendered plot
# grid.remove("line", grep = TRUE, global = TRUE)

# grid.draw(panel)     # Draw the edited panel - on top of the clipping region 


# Turn off the graphics device
dev.off()

# Find text.svg in your working directory




编辑:使用数据点绘制时的坐标系定义剪切区域。

library(ggplot2)
library(gridSVG)
library(grid)

# Open the graphics device
gridsvg(name = "test.svg")

# Create a plot
p <- ggplot(diamonds[1:300, ], aes(carat, price)) + 
       geom_point(aes(colour = cut)) +
       geom_line(data = data.frame(x = c(.3, .9), y = c(500, 2500)), aes(x,y), col = "skyblue", size = 2)
g <- ggplotGrob(p) # Store the plot as a grob


g = grid.force(g)  # So that grid sees all grobs
grid.draw(g)       # Draw the plot

# Get axis limits (including any expansion)
axis.limits = summarise_layout(ggplot_build(p))[1, c('xmin', 'xmax', 'ymin', 'ymax')]

# Find the 'panel' viewport,
# then push to a new viewport, 
# one that exactly overlaps the 'panel' viewport,
# but with limits on the x and y scales that are the same
# as the limits for the original ggplot. 
seekViewport(grep("panel.[0-9]", grid.ls(g)$name, value = TRUE))
pushViewport(dataViewport(xscale = axis.limits[1, 1:2],
                          yscale = axis.limits[1, 3:4]))

# Define the clipping path
 pg <- polygonGrob(x = c(.6,   0.3, .3,   .8,   1.2), 
                   y = c(500, 1500, 2900, 2900, 1500), 
                   default.units="native")
cp <- clipPath(pg)

# Add the clipping path to the points grob.
# That is, only the points inside the polygon will be visible,
# but the background and grid lines will not be clipped. 
# Nor will the blue line be clipped.
# grid.ls(g)     # names of the grobs

grid.clipPath("points", cp, grep = TRUE)   

# To clip the blue line, uncomment the next line
 grid.clipPath("GRID.polyline", cp, grep = TRUE)       

# To show the clipping region. 
 showcp = editGrob(pg, gp = gpar(fill = rgb(0, 0, 0, 0.05), col = "transparent"))
 grid.draw(showcp)

# And to move the grid lines and remaining data points in front of the clipping region.
 panel = grid.get("panel", grep = TRUE)   # Get the panel, and remove the background grob
 panel = removeGrob(panel, "background", grep = TRUE)

 grid.remove("points", grep = TRUE)     # Remove points and grid lines from the rendered plot
 grid.remove("line", grep = TRUE, global = TRUE)

 grid.draw(panel)     # Draw the edited panel - on top of the clipping region 


# Turn off the graphics device
dev.off()

# Find text.svg in your working directory

谢谢你的回答,它让事情更清晰并打开了选项。为了明确,剪切多边形的坐标是在视口坐标系中吗?另外,通过修改grep函数的参数,我可以选择我想要的几何图形(这里选择面板的所有元素,因此所有几何图形都被选择了?) - Hobo Sheep
第一个问题。坐标以“npc”单位表示:(0, 0)为左下角,(1, 1)为右上角。但是可以使用绘制数据时的坐标来定义剪切区域。我将添加编辑以展示如何完成这个过程。 - Sandy Muspratt
第二个问题继续。seekViewport 命令意味着我移动了所有面板 grobs 绘制的视口:网格线、背景、数据点和蓝线。在这个阶段,我还没有选择任何 grobs(或 geoms)。 - Sandy Muspratt
第二个问题继续。grid.clipPath("points", cp, grep = TRUE)将剪切区应用于点。蓝线保持不变。grid.clipPath("GRID.polyline", cp, grep = TRUE)将剪切区应用于线条。 - Sandy Muspratt
1
感谢您提供的所有有用信息。由于现在时间不多,我将在下周稍后查看它,并且如果我有其他问题(并且您不介意回答),我可能会再次向您寻求帮助。您真是太棒了,再次感谢。 - Hobo Sheep
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接